Статьи

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

Теперь мы собираемся открыть, вероятно, лучшую часть серии: как справиться с молнией !

В предыдущей части мы исправили случайный цвет для каждого лица, чтобы видеть сетку. Теперь мы собираемся изменить это, чтобы вычислить угол между светом и каждым лицом для лучшего освещения. Первый рассмотренный алгоритм называется Flat Shading . Он использует для лица нормали . Мы все еще увидим полигоны, использующие этот подход. Но благодаря Гуро Шейдингу мы пойдем еще дальше. Этот использует для каждой вершины нормали . Затем он будет интерполировать цвет на пиксель, используя 3 нормали.

В конце этого урока у вас должен получиться очень крутой рендеринг:

Плоская заливка

концепция

Чтобы иметь возможность применить алгоритм плоского затенения, нам сначала нужно вычислить вектор нормали лица. Как только мы его получим, нам нужно знать угол между этим нормальным вектором и вектором света. Чтобы быть более точным, мы будем использовать точечное произведение , которое даст нам косинус угла между этими двумя векторами. Поскольку это значение может быть от -1 до 1, мы будем сокращать его от 0 до 1. Это окончательное значение будет использоваться для применения количества света, применяемого к нашему лицу на основе его текущего цвета. В заключение, окончательный цвет нашего лица будет = color * Math.Max ​​(0, cos (angle) ).

Начнем с нормального вектора. В википедии определение Normal (геометрия) гласит: « Для выпуклого многоугольник (такой как треугольник ), нормаль поверхности может быть вычислена как векторное произведение двух (непараллельных) ребер многоугольника ».

Чтобы проиллюстрировать это, вы можете увидеть интересную фигуру из документации Blender : Blender 3D: Noob to Pro — Normal_coordinates

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

Откройте Blender, загрузите сетку Сюзанны, переключитесь в « Режим редактирования »:

образ

Откройте свойства сетки, нажав на нее и нажав «N». Под « Mesh Display » вы найдете 2 кнопки для нормалей. Нажмите на тот, который говорит « Отображать нормали лица в виде линий »:

образ

Вы получите что-то вроде этого:

образ

Нам нужно определить свет после этого. Наш свет для этих уроков будет самым простым: точечный свет . Точечный свет — это просто 3D-точка (Vector3). Количество света, которое получит наше лицо, будет одинаковым на любом расстоянии от света. Затем мы просто изменим интенсивность на основе угла между вектором нормали и вектором, сделанным из этого точечного источника света и центром нашего лица.

Таким образом, направление света будет: lightPosition — centerFacePosition -> это даст нам вектор направления света . Чтобы вычислить угол между этим световым вектором и вектором нормали , мы будем использовать точечное произведение: http://en.wikipedia.org/wiki/Dot_product

Рисунок взят из: Per-Pixel Lighting (статья Джона Чепмена)

Код

Обычно нам нужно сначала вычислить нормальный вектор. К счастью, Blender вычисляет эти нормы для нас. Более того, он экспортирует нормали для каждой вершины, которые мы будем использовать во второй части. Итак, чтобы вычислить наш вектор нормалей, нам просто нужно взять 3 нормали вершин, сложить их друг с другом и разделить на 3.

Нам нужно выполнить несколько задач по рефакторингу нашего кода, чтобы иметь возможность обрабатывать концепции, описанные ранее. До сих пор мы использовали только массив Vector3 для вершин. Этого уже недостаточно. Нам нужно больше данных: нормаль, связанная с вершиной (для затенения Гуро) и трехмерные проекционные координаты. Действительно, проекция в настоящее время выполняется только в 2D. Нам нужно сохранять трехмерные координаты, проецируемые в трехмерный мир, чтобы иметь возможность вычислять различные векторы.

Затем мы создадим структуру, содержащую 3 Vector3: текущие координаты, которые мы использовали до сих пор, нормаль к вершине и мировые координаты.

Метод ProcessScanLine также должен будет интерполировать больше данных (таких как нормали по вершинам в затенении Гуро). Поэтому мы создаем для этого структуру ScanLineData .

  общедоступная сетка 
  { 
     публичная строка Name { get ;  установить ;  } 
     public Vertex [] Vertices { get ;  приватный набор ;  } 
     публичное лицо [] Faces { get ;  установить ;  } 
     public Vector3 Position { get ;  установить ;  } 
     public Vector3 Rotation { get ;  установить ;  } 
      общедоступная сетка ( имя строки , int verticesCount, int FaceCount) 
     { 
         Вершины = новая вершина [verticesCount]; 
         Faces = новое лицо [FaceCount]; 
         Имя = имя; 
     } 
  публичная структура Vertex 
  { 
     общедоступный Vector3 Normal; 
     общедоступные координаты Vector3 ; 
     общедоступная Vector3 WorldCoordinates; 
 

  публичная структура ScanLineData 
  { 
     public int currentY; 
     публичное плавание ndotla; 
     публичный поплавок ndotlb; 
     публичный поплавок ndotlc; 
     публичное плавание ndotld; 
 

Никаких изменений по сравнению с кодом предыдущего урока в JS, поскольку у нас нет введенных значений

Это создает различные небольшие модификации кода. Первый — это способ загрузить файл JSON, экспортированный Blender. Теперь нам нужно загрузить нормали для каждой вершины и построить объекты Vertex вместо объектов Vector3 в массиве Vertices:

  // Сначала заполняем массив вершин нашей сетки 
  для ( var index = 0; index <verticesCount; index ++) 
     var x = ( float ) verticesArray [index * verticesStep] .Value; 
     var y = ( float ) verticesArray [index * verticesStep + 1] .Value; 
     var z = ( float ) verticesArray [index * verticesStep + 2] .Value; 
     // Загрузка нормали вершины, экспортируемой Blender 
     var nx = ( float ) verticesArray [index * verticesStep + 3] .Value; 
     var ny = ( float ) verticesArray [index * verticesStep + 4] .Value; 
     var nz = ( float ) verticesArray [index * verticesStep + 5] .Value; 
     mesh.Vertices [index] = новая вершина {Coordinates = new Vector3 (x, y, z), Normal = новый Vector3 (nx, ny, nz)}; 
 

Вот все методы / функции, которые были обновлены:

Project () теперь работает над структурой Vertex и проецирует координаты вершин в 3D (используя World Matrix), а также проецирует по нормали на вершину.

DrawTriangle () теперь получает некоторые структуры Vertex в качестве входных данных, вычисляет NDotL с помощью метода ComputeNDotL и вызывает ProcessScanLine с этими данными

ComputeNDotL () вычисляет косинус угла между нормалью и направлением света

ProcessScanLine () теперь меняет цвет, используя значение NDotL, отправленное DrawTriangle. В настоящее время у нас есть только 1 цвет на треугольник, так как мы используем Flat Shading.

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

  // Проект берет несколько трехмерных координат и преобразует их 
  / в 2D координатах с использованием матрицы преобразования 
  / Он также преобразовывает те же координаты и норму в вершину  
  / в 3D мире 
  общедоступный проект вершин ( Vertex vertex, Matrix transMat, Matrix world) 
     // преобразование координат в 2D пространство 
     var point2d = Vector3 .TransformCoordinate (vertex.Coordinates, transMat); 
     // преобразование координат и нормали к вершине в трехмерном мире 
     var point3dWorld = Vector3 .TransformCoordinate (vertex.Coordinates, world); 
     var normal3dWorld = Vector3 .TransformCoordinate (vertex.Normal, world); 
      // Преобразованные координаты будут основаны на системе координат 
     // начиная с центра экрана.  Но рисование на экране обычно начинается 
     // сверху слева.  Затем нам нужно преобразовать их снова, чтобы в верхнем левом углу были значения x: 0, y: 0. 
     var x = point2d.X * renderWidth + renderWidth / 2.0f; 
     var y = -point2d.Y * renderHeight + renderHeight / 2.0f; 
      вернуть новую вершину 
     { 
         Координаты = новый вектор3 (x, y, point2d.Z), 
         Normal = normal3dWorld, 
         WorldCoordinates = point3dWorld 
     }; 
  // рисуем линию между 2 точками слева направо 
  / papb -> pcpd 
  / pa, pb, pc, pd должны быть отсортированы до 
  void ProcessScanLine ( данные ScanLineData , Vertex va, Vertex vb, Vertex vc, Vertex vd, Color4 color) 
     Vector3 pa = va.Coordinates; 
     Vector3 pb = vb.Coordinates; 
     Vector3 pc = vc.Coordinates; 
     Vector3 pd = vd.Coordinates; 
      // Благодаря текущему Y мы можем вычислить градиент для вычисления других значений, таких как 
     // начальный X (sx) и конечный X (ex) для рисования между 
     // если pa.Y == pb.Y или pc.Y == pd.Y, градиент равен 1 
     вар градиент1 = pa.Y! = pb.Y?  (data.currentY - pa.Y) / (pb.Y - pa.Y): 1; 
     вар градиент2 = стр.Y! = pd.Y?  (data.currentY - pc.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, градиент); 
         var ndotl = data.ndotla; 
         // изменение значения цвета с использованием косинуса угла 
         // между вектором света и вектором нормали 
         DrawPoint ( новый Vector3 (x, data.currentY, z), цвет * ndotl); 
     } 
  // Вычисляем косинус угла между вектором света и вектором нормали 
  / Возвращает значение от 0 до 1 
  float ComputeNDotL ( вершина Vector3, нормаль Vector3, позиция Light Vector3)  
     var lightDirection = lightPosition - вершина; 
      normal.Normalize (); 
     lightDirection.Normalize (); 
      return Math .Max (0, Vector3 .Dot (normal, lightDirection)); 
  публичный void DrawTriangle ( вершина v1, вершина v2, вершина v3, цвет Color4 ) 
     // Сортировка точек, чтобы всегда иметь этот порядок на экране p1, p2 & p3 
     // с p1 всегда вверх (таким образом, Y имеет наименьший возможный, чтобы быть около верхнего экрана) 
     // затем p2 между p1 и p3 
     if (v1.Coordinates.Y> v2.Coordinates.Y) 
     { 
         var temp = v2; 
         v2 = v1; 
         v1 = температура; 
     } 
      if (v2.Coordinates.Y> v3.Coordinates.Y) 
     { 
         var temp = v2; 
         v2 = v3; 
         v3 = температура; 
     } 
      if (v1.Coordinates.Y> v2.Coordinates.Y) 
     { 
         var temp = v2; 
         v2 = v1; 
         v1 = температура; 
     } 
      Vector3 p1 = v1.Coordinates; 
     Vector3 p2 = v2.Coordinates; 
     Vector3 p3 = v3.Coordinates; 
      // вектор нормальной грани - это средняя норма между нормалями каждой вершины 
     // вычисляем также центральную точку лица 
     Vector3 vnFace = (v1.Normal + v2.Normal + v3.Normal) / 3; 
     Vector3 centerPoint = (v1.WorldCoordinates + v2.WorldCoordinates + v3.WorldCoordinates) / 3; 
     // Светлая позиция  
     Vector3 lightPos = new Vector3 (0, 10, 10); 
     // вычисление cos угла между вектором света и вектором нормали 
     // он вернет значение от 0 до 1, которое будет использоваться как интенсивность цвета 
     float ndotl = ComputeNDotL (centerPoint, vnFace, lightPos); 
      var data = new ScanLineData {ndotla = ndotl}; 
      // вычисление направлений линий 
     поплавок 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 ++) 
         { 
             data.currentY = y; 
              если (у <p2.Y) 
             { 
                 ProcessScanLine (данные, v1, v3, v1, v2, цвет); 
             } 
             еще 
             { 
                 ProcessScanLine (данные, v1, v3, v2, v3, цвет); 
             } 
         } 
     } 
     // Первый случай, когда треугольники такие: 
     // P1 
     // - 
     // -  
     // - - 
     // - - 
     // P2 - -  
     // - - 
     // - - 
     // - 
     // P3 
     еще 
     { 
         для ( var y = ( int ) p1.Y; y <= ( int ) p3.Y; y ++) 
         { 
             data.currentY = y; 
              если (у <p2.Y) 
             { 
                 ProcessScanLine (данные, v1, v2, v1, v3, цвет); 
             } 
             еще 
             { 
                 ProcessScanLine (данные, v2, v3, v1, v3, цвет); 
             } 
         } 
     } 
 

  // Проект берет несколько трехмерных координат и преобразует их 
  / в 2D координатах с использованием матрицы преобразования 
  / Он также преобразует те же координаты и нормаль к вершине  
  / в 3D мире 
  открытый проект (вершина: Vertex, transMat: BABYLON.Matrix, мир: BABYLON.Matrix): Vertex { 
     // преобразование координат в 2D пространство 
     var point2d = BABYLON.Vector3.TransformCoordinates (vertex.Coordinates, transMat); 
     // преобразование координат и нормали к вершине в трехмерном мире 
     var point3DWorld = BABYLON.Vector3.TransformCoordinates (vertex.Coordinates, world); 
     var normal3DWorld = BABYLON.Vector3.TransformCoordinates (vertex.Normal, world); 
      // Преобразованные координаты будут основаны на системе координат 
     // начиная с центра экрана.  Но рисование на экране обычно начинается 
     // сверху слева.  Затем нам нужно преобразовать их снова, чтобы в верхнем левом углу были значения x: 0, y: 0. 
     var x = point2d.x * this .workingWidth + this .workingWidth / 2.0; 
     var y = -point2d.y * this .workingHeight + this .workingHeight / 2.0; 
      возврат ({ 
         Координаты: новый BABYLON.Vector3 (x, y, point2d.z), 
         Normal: normal3DWorld, 
         WorldCoordinates: point3DWorld 
     }); 
  // рисуем линию между 2 точками слева направо 
  / papb -> pcpd 
  / pa, pb, pc, pd должны быть отсортированы до 
  public processScanLine (данные: ScanLineData, va: Vertex, vb: Vertex, 
vc: вершина, vd: вершина, цвет: BABYLON.Color4): void { var pa = va.Coordinates; var pb = vb.Coordinates; var pc = vc.Coordinates; var pd = vd.Coordinates; // Благодаря текущему Y мы можем вычислить градиент для вычисления других значений, таких как // начальный X (sx) и конечный X (ex) для рисования между // если pa.Y == pb.Y или pc.Y == pd.Y, градиент равен 1 var градиент1 = pa.y! = pb.y? (data.currentY - pa.y) / (pb.y - pa.y): 1; var градиент2 = pc.y! = pd.y? (data.currentY - 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, Градиент); var ndotl = data.ndotla; // изменение значения цвета с использованием косинуса угла // между вектором света и вектором нормали this .drawPoint ( новый BABYLON.Vector3 (x, data.currentY, z),
новый BABYLON.Color4 (color.r * ndotl, color.g * ndotl, color.b * ndotl, 1)); } // Вычисляем косинус угла между вектором света и вектором нормали / Возвращает значение от 0 до 1 public computeNDotL (вершина: BABYLON.Vector3, нормальная: BABYLON.Vector3,
lightPosition: BABYLON.Vector3): number { var lightDirection = lightPosition.subtract (vertex); normal.normalize (); lightDirection.normalize (); return Math.max (0, BABYLON.Vector3.Dot (normal, lightDirection)); public drawTriangle (v1: вершина, v2: вершина, v3: вершина, цвет: BABYLON.Color4): void { // Сортировка точек, чтобы всегда иметь этот порядок на экране p1, p2 & p3 // с p1 всегда вверх (таким образом, Y имеет наименьший возможный, чтобы быть около верхнего экрана) // затем p2 между p1 и p3 if (v1.Coordinates.y> v2.Coordinates.y) { var temp = v2; v2 = v1; v1 = температура; } if (v2.Coordinates.y> v3.Coordinates.y) { var temp = v2; v2 = v3; v3 = температура; } if (v1.Coordinates.y> v2.Coordinates.y) { var temp = v2; v2 = v1; v1 = температура; } var p1 = v1.Coordinates; var p2 = v2.Coordinates; var p3 = v3.Coordinates; // вектор нормальной грани - это средняя норма между нормалями каждой вершины // вычисляем также центральную точку лица var vnFace = (v1.Normal.add (v2.Normal.add (v3.Normal))). scale (1/3); var centerPoint = (v1.WorldCoordinates.add (v2.WorldCoordinates.add (v3.WorldCoordinates))). scale (1/3); // Светлая позиция var lightPos = new BABYLON.Vector3 (0, 10, 10); // вычисление cos угла между вектором света и вектором нормали // он вернет значение от 0 до 1, которое будет использоваться как интенсивность цвета var ndotl = this .computeNDotL (centerPoint, vnFace, lightPos); var data: ScanLineData = {ndotla: ndotl}; // вычисление направлений линий 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 ++) { data.currentY = y; if (y <p2.y) { это .processScanLine (данные, v1, v3, v1, v2, цвет); } еще { это .processScanLine (данные, v1, v3, v2, v3, цвет); } } } // Первый случай, когда треугольники такие: // P1 // - // - // - - // - - // P2 - - // - - // - - // - // P3 еще { for ( var y = p1.y >> 0; y <= p3.y >> 0; y ++) { data.currentY = y; if (y <p2.y) { это .processScanLine (данные, v1, v2, v1, v3, цвет); } еще { это .processScanLine (данные, v2, v3, v1, v3, цвет); } } }

  // Проект берет несколько трехмерных координат и преобразует их 
  / в 2D координатах с использованием матрицы преобразования 
  / Он также преобразует те же координаты и нормаль к вершине  
  / в 3D мире 
  Device.prototype.project = function (vertex, transMat, world) { 
     // преобразование координат в 2D пространство 
     var point2d = BABYLON.Vector3.TransformCoordinates (vertex.Coordinates, transMat); 
     // преобразование координат и нормали к вершине в трехмерном мире 
     var point3DWorld = BABYLON.Vector3.TransformCoordinates (vertex.Coordinates, world); 
     var normal3DWorld = BABYLON.Vector3.TransformCoordinates (vertex.Normal, world); 
      // Преобразованные координаты будут основаны на системе координат 
     // начиная с центра экрана.  Но рисование на экране обычно начинается 
     // сверху слева.  Затем нам нужно преобразовать их снова, чтобы в верхнем левом углу были значения x: 0, y: 0. 
     var x = point2d.x * this .workingWidth + this .workingWidth / 2.0; 
     var y = -point2d.y * this .workingHeight + this .workingHeight / 2.0; 
      возврат ({ 
         Координаты: новый BABYLON.Vector3 (x, y, point2d.z), 
         Normal: normal3DWorld, 
         WorldCoordinates: point3DWorld 
     }); 
  ; 
  // рисуем линию между 2 точками слева направо 
  / papb -> pcpd 
  / pa, pb, pc, pd должны быть отсортированы до 
  Device.prototype.processScanLine = function (data, va, vb, vc, vd, color) { 
     var pa = va.Coordinates; 
     var pb = vb.Coordinates; 
     var pc = vc.Coordinates; 
     var pd = vd.Coordinates; 
      // Благодаря текущему Y мы можем вычислить градиент для вычисления других значений, таких как 
     // начальный X (sx) и конечный X (ex) для рисования между 
     // если pa.Y == pb.Y или pc.Y == pd.Y, градиент равен 1 
     var градиент1 = pa.y! = pb.y?  (data.currentY - pa.y) / (pb.y - pa.y): 1; 
     var градиент2 = pc.y! = pd.y?  (data.currentY - 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) вправо ( 
     для ( var x = sx; x <ex; x ++) { 
         градиент var = (x - sx) / (ex - sx); 
          var z = this .interpolate (z1, z2, Градиент); 
         var ndotl = data.ndotla; 
         // изменение значения цвета с использованием косинуса угла 
         // между вектором света и вектором нормали 
         this .drawPoint ( новый BABYLON.Vector3 (x, data.currentY, z), 
новый BABYLON.Color4 (color.r * ndotl, color.g * ndotl, color.b * ndotl, 1)); } ; // Вычисляем косинус угла между вектором света и вектором нормали / Возвращает значение от 0 до 1 Device.prototype.computeNDotL = function (vertex, normal, lightPosition) { var lightDirection = lightPosition.subtract (vertex); normal.normalize (); lightDirection.normalize (); return Math.max (0, BABYLON.Vector3.Dot (normal, lightDirection)); ; Device.prototype.drawTriangle = function (v1, v2, v3, color) { // Сортировка точек, чтобы всегда иметь этот порядок на экране p1, p2 & p3 // с p1 всегда вверх (таким образом, Y имеет наименьший возможный, чтобы быть около верхнего экрана) // затем p2 между p1 и p3 if (v1.Coordinates.y> v2.Coordinates.y) { var temp = v2; v2 = v1; v1 = температура; } if (v2.Coordinates.y> v3.Coordinates.y) { var temp = v2; v2 = v3; v3 = температура; } if (v1.Coordinates.y> v2.Coordinates.y) { var temp = v2; v2 = v1; v1 = температура; } var p1 = v1.Coordinates; var p2 = v2.Coordinates; var p3 = v3.Coordinates; // вектор нормальной грани - это средняя норма между нормалями каждой вершины // вычисляем также центральную точку лица var vnFace = (v1.Normal.add (v2.Normal.add (v3.Normal))). scale (1/3); var centerPoint = (v1.WorldCoordinates.add (v2.WorldCoordinates.add (v3.WorldCoordinates))). scale (1/3); // Светлая позиция var lightPos = new BABYLON.Vector3 (0, 10, 10); // вычисление cos угла между вектором света и вектором нормали // он вернет значение от 0 до 1, которое будет использоваться как интенсивность цвета var ndotl = this .computeNDotL (centerPoint, vnFace, lightPos); var data = {ndotla: ndotl}; // вычисление направлений линий 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 ++) { data.currentY = y; if (y <p2.y) { это .processScanLine (данные, v1, v3, v1, v2, цвет); } еще { это .processScanLine (данные, v1, v3, v2, v3, цвет); } } } // Первый случай, когда треугольники такие: // P1 // - // - // - - // - - // P2 - - // - - // - - // - // P3 еще { for ( var y = p1.y >> 0; y <= p3.y >> 0; y ++) { data.currentY = y; if (y <p2.y) { это .processScanLine (данные, v1, v2, v1, v3, цвет); } еще { это .processScanLine (данные, v2, v3, v1, v3, цвет); } } } ;

Чтобы просмотреть результат в своем браузере, нажмите на скриншот ниже:

Flat Shading демонстрационный 3D-движок в HTML5

3D Software Engine: посмотрите демонстрацию Flat Shading в HTML5 в вашем браузере

На моем Lenovo X1 Carbon (Core i7 Ivy Bridge) я выполняю этот рендеринг 640 × 480 со скоростью около 35 кадров в секунду в Internet Explorer 11 (который является самым быстрым браузером для этой демонстрации на моем компьютере с Windows 8.1) и около 4 кадров в секунду на IE11 на поверхности RT. Параллельная версия C # запускает ту же сцену в 640 × 480 со скоростью 60 FPS.

Вы можете скачать решение, реализующее эту плоскую заливку здесь:

C # : SoftEngineCSharpPart5FlatShading.zip

TypeScript : SoftEngineTSPart5FlatShading.zip

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

Gouraud Shading

концепция

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

нормативы

Извлечено из: Учебное пособие 5. Ландшафт — свет и вершина нормального вектора

На этом рисунке вы можете видеть различия между плоской штриховкой и штриховкой Гуро. В квартире используется уникальная центрированная норма, а в гуро — 3 нормали. Вы также видите на трехмерной сетке (пирамиде), что нормаль на вершину на грань. Я имею в виду, что одна и та же вершина будет иметь разные нормали в зависимости от грани, которую мы сейчас рисуем.

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

Затенение Гуро: Освещение происходит только в вершинах.  Пиксели подсвечиваются линейной интерполяцией

Извлечено из: Учебники — создание нормальных карт Бен Клоуард

На этом рисунке представьте, что верхняя нормаль вершины имеет угол> 90 градусов относительно направления света, тогда ее цвет должен быть черным (минимальный уровень света = 0). Теперь представьте, что две другие нормальные вершины имеют угол 0 градусов относительно направления света, это означает, что они должны получить максимальный уровень света (1).

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

Код

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

  // рисуем линию между 2 точками слева направо 
  / papb -> pcpd 
  / pa, pb, pc, pd должны быть отсортированы до 
  void ProcessScanLine ( данные ScanLineData , Vertex va, Vertex vb, Vertex vc, Vertex vd, Color4 color) 
     Vector3 pa = va.Coordinates; 
     Vector3 pb = vb.Coordinates; 
     Vector3 pc = vc.Coordinates; 
     Vector3 pd = vd.Coordinates; 
      // Благодаря текущему Y мы можем вычислить градиент для вычисления других значений, таких как 
     // начальный X (sx) и конечный X (ex) для рисования между 
     // если pa.Y == pb.Y или pc.Y == pd.Y, градиент равен 1 
     вар градиент1 = pa.Y! = pb.Y?  (data.currentY - pa.Y) / (pb.Y - pa.Y): 1; 
     вар градиент2 = стр.Y! = pd.Y?  (data.currentY - pc.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); 
      var snl = Interpolate (data.ndotla, data.ndotlb, градиент1); 
     var inv = Interpolate (data.ndotlc, data.ndotld, градиент2); 
      // рисуем линию слева (sx) вправо (ex)  
     для ( var x = sx; x <ex; x ++) 
     { 
         градиент плавания = (x - sx) / ( плавание ) (ex - sx); 
          var z = интерполировать (z1, z2, градиент); 
         var ndotl = интерполировать (snl, inv, градиент); 
         // изменение значения цвета с использованием косинуса угла 
         // между вектором света и вектором нормали 
         DrawPoint ( новый Vector3 (x, data.currentY, z), цвет * ndotl); 
     } 
  публичный void DrawTriangle ( вершина v1, вершина v2, вершина v3, цвет Color4 ) 
     // Сортировка точек, чтобы всегда иметь этот порядок на экране p1, p2 & p3 
     // с p1 всегда вверх (таким образом, Y имеет наименьший возможный, чтобы быть около верхнего экрана) 
     // затем p2 между p1 и p3 
     if (v1.Coordinates.Y> v2.Coordinates.Y) 
     { 
         var temp = v2; 
         v2 = v1; 
         v1 = температура; 
     } 
      if (v2.Coordinates.Y> v3.Coordinates.Y) 
     { 
         var temp = v2; 
         v2 = v3; 
         v3 = температура; 
     } 
      if (v1.Coordinates.Y> v2.Coordinates.Y) 
     { 
         var temp = v2; 
         v2 = v1; 
         v1 = температура; 
     } 
      Vector3 p1 = v1.Coordinates; 
     Vector3 p2 = v2.Coordinates; 
     Vector3 p3 = v3.Coordinates; 
      // Светлая позиция  
     Vector3 lightPos = new Vector3 (0, 10, 10); 
     // вычисление cos угла между вектором света и вектором нормали 
     // он вернет значение от 0 до 1, которое будет использоваться как интенсивность цвета 
     float nl1 = ComputeNDotL (v1.WorldCoordinates, v1.Normal, lightPos); 
     float nl2 = ComputeNDotL (v2.WorldCoordinates, v2.Normal, lightPos); 
     float nl3 = ComputeNDotL (v3.WorldCoordinates, v3.Normal, lightPos); 
      var data = new ScanLineData {}; 
      // вычисление направлений линий 
     поплавок 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; 
      если (dP1P2> dP1P3) 
     { 
         для ( var y = ( int ) p1.Y; y <= ( int ) p3.Y; y ++) 
         { 
             data.currentY = y; 
              если (у <p2.Y) 
             { 
                 data.ndotla = nl1; 
                 data.ndotlb = nl3; 
                 data.ndotlc = nl1; 
                 data.ndotld = nl2; 
                 ProcessScanLine (данные, v1, v3, v1, v2, цвет); 
             } 
             еще 
             { 
                 data.ndotla = nl1; 
                 data.ndotlb = nl3; 
                 data.ndotlc = nl2; 
                 data.ndotld = nl3; 
                 ProcessScanLine (данные, v1, v3, v2, v3, цвет); 
             } 
         } 
     } 
     еще 
     { 
         для ( var y = ( int ) p1.Y; y <= ( int ) p3.Y; y ++) 
         { 
             data.currentY = y; 
              если (у <p2.Y) 
             { 
                 data.ndotla = nl1; 
                 data.ndotlb = nl2; 
                 data.ndotlc = nl1; 
                 data.ndotld = nl3; 
                 ProcessScanLine (данные, v1, v2, v1, v3, цвет); 
             } 
             еще 
             { 
                 data.ndotla = nl2; 
                 data.ndotlb = nl3; 
                 data.ndotlc = nl1; 
                 data.ndotld = nl3; 
                 ProcessScanLine (данные, v2, v3, v1, v3, цвет); 
             } 
         } 
     } 
 

  // рисуем линию между 2 точками слева направо 
  / papb -> pcpd 
  / pa, pb, pc, pd должны быть отсортированы до 
  public processScanLine (данные: ScanLineData, va: Vertex, vb: Vertex, 
vc: вершина, vd: вершина, цвет: BABYLON.Color4): void { var pa = va.Coordinates; var pb = vb.Coordinates; var pc = vc.Coordinates; var pd = vd.Coordinates; // Благодаря текущему Y мы можем вычислить градиент для вычисления других значений, таких как // начальный X (sx) и конечный X (ex) для рисования между // если pa.Y == pb.Y или pc.Y == pd.Y, градиент равен 1 var градиент1 = pa.y! = pb.y? (data.currentY - pa.y) / (pb.y - pa.y): 1; var градиент2 = pc.y! = pd.y? (data.currentY - 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); var snl = this .interpolate (data.ndotla, data.ndotlb, градиент1); var inv = this .interpolate (data.ndotlc, data.ndotld, градиент2); // рисуем линию слева (sx) вправо (ex) для ( var x = sx; x <ex; x ++) { градиент var : число = (x - sx) / (ex - sx); var z = this .interpolate (z1, z2, Градиент); var ndotl = this .interpolate (snl, inv, градиент); // изменение значения цвета с использованием косинуса угла // между вектором света и вектором нормали this .drawPoint ( новый BABYLON.Vector3 (x, data.currentY, z),
новый BABYLON.Color4 (color.r * ndotl, color.g * ndotl, color.b * ndotl, 1)); } public drawTriangle (v1: вершина, v2: вершина, v3: вершина, цвет: BABYLON.Color4): void { // Сортировка точек, чтобы всегда иметь этот порядок на экране p1, p2 & p3 // с p1 всегда вверх (таким образом, Y имеет наименьший возможный, чтобы быть около верхнего экрана) // затем p2 между p1 и p3 if (v1.Coordinates.y> v2.Coordinates.y) { var temp = v2; v2 = v1; v1 = температура; } if (v2.Coordinates.y> v3.Coordinates.y) { var temp = v2; v2 = v3; v3 = температура; } if (v1.Coordinates.y> v2.Coordinates.y) { var temp = v2; v2 = v1; v1 = температура; } var p1 = v1.Coordinates; var p2 = v2.Coordinates; var p3 = v3.Coordinates; // Светлая позиция var lightPos = new BABYLON.Vector3 (0, 10, 10); // вычисление cos угла между вектором света и вектором нормали // он вернет значение от 0 до 1, которое будет использоваться как интенсивность цвета // var ndotl = this.computeNDotL (centerPoint, vnFace, lightPos); var nl1 = this .computeNDotL (v1.WorldCoordinates, v1.Normal, lightPos); var nl2 = this .computeNDotL (v2.WorldCoordinates, v2.Normal, lightPos); var nl3 = this .computeNDotL (v3.WorldCoordinates, v3.Normal, lightPos); var data: ScanLineData = {}; // вычисление направлений линий 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; если (dP1P2> dP1P3) { for ( var y = p1.y >> 0; y <= p3.y >> 0; y ++) { data.currentY = y; if (y <p2.y) { data.ndotla = nl1; data.ndotlb = nl3; data.ndotlc = nl1; data.ndotld = nl2; это .processScanLine (данные, v1, v3, v1, v2, цвет); } еще { data.ndotla = nl1; data.ndotlb = nl3; data.ndotlc = nl2; data.ndotld = nl3; это .processScanLine (данные, v1, v3, v2, v3, цвет); } } } еще { for ( var y = p1.y >> 0; y <= p3.y >> 0; y ++) { data.currentY = y; if (y <p2.y) { data.ndotla = nl1; data.ndotlb = nl2; data.ndotlc = nl1; data.ndotld = nl3; это .processScanLine (данные, v1, v2, v1, v3, цвет); } еще { data.ndotla = nl2; data.ndotlb = nl3; data.ndotlc = nl1; data.ndotld = nl3; это .processScanLine (данные, v2, v3, v1, v3, цвет); } } }

  // рисуем линию между 2 точками слева направо 
  / papb -> pcpd 
  / pa, pb, pc, pd должны быть отсортированы до 
  Device.prototype.processScanLine = function (data, va, vb, vc, vd, color) { 
     var pa = va.Coordinates; 
     var pb = vb.Coordinates; 
     var pc = vc.Coordinates; 
     var pd = vd.Coordinates; 
      // Благодаря текущему Y мы можем вычислить градиент, чтобы вычислить другие значения, такие как 
   // начальный X (sx) и конечный X (ex) для рисования между
   // если pa.Y == pb.Y или pc.Y == pd.Y, градиент равен 1
    var  градиент1 = pa.y! = pb.y? (data.currentY - pa.y) / (pb.y - pa.y): 1;
     var  градиент2 = pc.y! = pd.y? (data.currentY - 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);
      var  snl =  this  .interpolate (data.ndotla, data.ndotlb, градиент1);
     var  eng =  это .interpolate (data.ndotlc, data.ndotld, градиент2);
     // рисуем линию слева (sx) вправо (ex) 
     для  (  var  x = sx; x <ex; x ++) {
         var  градиент = (x - sx) / (ex - sx);
         var  z =  this  .interpolate (z1, z2, Градиент);
        var  ndotl =  this  .interpolate (snl, eng, градиент);
        // изменение значения цвета с использованием косинуса угла
       // между вектором света и вектором нормали
        this  .drawPoint (  новый  BABYLON.Vector3 (x, data.currentY, z), 
новый BABYLON.Color4 (color.r * ndotl, color.g * ndotl, color.b * ndotl, 1)); } ; Device.prototype.drawTriangle = function (v1, v2, v3, color) { // Сортировка точек, чтобы всегда иметь этот порядок на экране p1, p2 & p3 // с p1 всегда вверх (таким образом, Y имеет наименьший возможный, чтобы быть около верхнего экрана) // затем p2 между p1 и p3 if (v1.Coordinates.y> v2.Coordinates.y) { var temp = v2; v2 = v1; v1 = температура; } if (v2.Coordinates.y> v3.Coordinates.y) { var temp = v2; v2 = v3; v3 = температура; } if (v1.Coordinates.y> v2.Coordinates.y) { var temp = v2; v2 = v1; v1 = температура; } var p1 = v1.Coordinates; var p2 = v2.Coordinates; var p3 = v3.Coordinates; // Положение света var lightPos = new BABYLON.Vector3 (0, 10, 10); // вычисление cos угла между вектором света и вектором нормали // он вернет значение от 0 до 1, которое будет использоваться как интенсивность цвета var nl1 = this .computeNDotL (v1.WorldCoordinates, v1.Normal, lightPos); var nl2 = this .computeNDotL (v2.WorldCoordinates, v2.Normal, lightPos); var nl3 = this .computeNDotL (v3.WorldCoordinates, v3.Normal, lightPos); var data = {}; // вычисление направлений линий 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; if (dP1P2> dP1P3) { for ( var y = p1.y >> 0; y <= p3.y >> 0; y ++) { data.currentY = y; if (y <p2.y) { data.ndotla = nl1; data.ndotlb = nl3; data.ndotlc = nl1; data.ndotld = nl2; это .processScanLine (данные, v1, v3, v1, v2, цвет); } еще { data.ndotla = nl1; data.ndotlb = nl3; data.ndotlc = nl2; data.ndotld = nl3; это .processScanLine (данные, v1, v3, v2, v3, цвет); } } } else { for ( var y = p1.y >> 0; y <= p3.y >> 0; y ++) { data.currentY = y; if (y <p2.y) { data.ndotla = nl1; data.ndotlb = nl2; data.ndotlc = nl1; data.ndotld = nl3; это .processScanLine (данные, v1, v2, v1, v3, цвет); } еще { data.ndotla = nl2; data.ndotlb = nl3; data.ndotlc = nl1; data.ndotld = nl3; это .processScanLine (данные, v2, v3, v1, v3, цвет); } } } ;

Чтобы просмотреть результат в своем браузере, нажмите на скриншот ниже:

Демонстрационная программа Gouraud Shading 3D Software в HTML5

3D Software Engine: посмотрите демонстрацию Gouraud Shading в HTML5 в вашем браузере

Вы увидите, что производительность / FPS почти такие же, как с алгоритмом Flat Shading, но с гораздо более приятным рендерингом. Существует еще лучший алгоритм с именем Phong Shading, использующий это время на пиксельные нормали.

Вот еще одна сцена для тестирования в вашем браузере HTML5. Он использует сетку Torus, экспортированную из Blender :

Gouraud Shading с тором в HTML5

3D Software engine: посмотрите демонстрацию Gouraud Shading с помощью Torus

Вы можете скачать решение, реализующее эту заливку Гуро, здесь:

C # : SoftEngineCSharpPart5GouraudShading.zip

TypeScript : SoftEngineTSPart5GouraudShading.zip

JavaScript : SoftEngineJSPart5GouraudShading.zip или просто щелкните правой кнопкой мыши -> для просмотра исходного кода в первом встроенном фрейме iframe.

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

Gouraud Shading & Texture mapping на нашей любимой Сюзанне

И мы также увидим точно такой же 3D-объект в движке WebGL. Затем вы поймете, почему GPU так важны для повышения производительности 3D-рендеринга в реальном времени!

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