Статьи

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

Теперь, когда мы создали ядро ​​нашего 3D-движка благодаря предыдущему уроку, часть 1 , мы можем работать над улучшением рендеринга. Следующий шаг — соединить точки, чтобы нарисовать некоторые линии, чтобы сделать то, что вы, вероятно, знаете, как «каркасный» рендеринг .

1 — Написание логики ядра для камеры, сетки и объекта устройства
2 — Рисование линий и треугольников для получения каркасного рендеринга (эта статья)
3 — Загрузка сеток, экспортированных из Blender в формате JSON
4 — Заполнение треугольника растеризацией и использованием Z-буфера
4b — Бонус: использование советов и параллелизма для повышения производительности
5 — Управление светом с помощью Flat Shading & Gouraud Shading
6 — Применение текстур, отбор задней поверхности и WebGL

В этом уроке вы узнаете, как рисовать линии, что такое лицо, и насколько крут алгоритм Брезенхэма для рисования некоторых треугольников.

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

Да! Наш 3D вращающийся куб начинает реально жить на наших экранах!

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

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

— если расстояние между двумя точками (точка0 и точка1) меньше 2 пикселей, делать нечего
— в противном случае мы находим среднюю точку между обеими точками (координаты точка0 + (координаты точка1 — координаты точка0) / 2)
— мы рисуем эту точку на экране
— мы запускаем этот алгоритм рекурсивно между точкой 0 и средней точкой и между средней точкой и точкой 1

Вот код для этого:

  общедоступная пустота DrawLine ( Vector2 point0, Vector2 point1) 
     var dist = (point1 - point0) .Length (); 
      // Если расстояние между двумя точками меньше 2 пикселей 
     // Мы выходим 
     if (dist <2) 
         возврат ; 
      // Находим среднюю точку между первой и второй точкой 
     Vector2 middlePoint = точка0 + (точка1 - точка0) / 2; 
     // Мы рисуем эту точку на экране 
     DrawPoint (middlePoint); 
     // Рекурсивный алгоритм запущен между первой и средней точкой 
     // и между средней и второй точкой 
     DrawLine (point0, middlePoint); 
     DrawLine (средняя точка, точка1); 
 

Вам нужно обновить цикл рендеринга, чтобы использовать этот новый фрагмент кода:

И теперь вы должны получить что-то вроде этого:

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

Отображение граней с треугольниками

Теперь, когда мы знаем, как рисовать линии, нам нужен лучший способ визуализации меша с ними. Самая простая геометрическая 2D-форма — это треугольник . Идея в 3D состоит в том, чтобы нарисовать все наши сетки с помощью этих треугольников. Затем нам нужно разделить каждую сторону нашего куба на 2 треугольника. Мы собираемся сделать это «вручную», но в следующем уроке мы увидим, что 3D-моделисты сейчас делают этот шаг автоматически для нас.

Чтобы нарисовать треугольники, вам нужно иметь 3 точки / вершины. Тогда грань — это просто структура, содержащая 3 значения, которые являются индексами, указывающими на правильный массив вершин меша, который будет отображаться.

Чтобы понять эту концепцию, давайте возьмем наш предыдущий рисунок с кубом, отображаемым Blender

image

На этой фигуре отображены 4 вершины со следующими индексами: 0, 1, 2, 3. Чтобы нарисовать верхнюю часть куба, нам нужно нарисовать 2 треугольника. Первый, Face 0, будет нарисован с 3 линиями от вершины 0 (-1, 1, 1) до вершины 1 (1, 1, 1), от вершины 1 (1, 1, 1) до вершины 2 (- 1, –1, 1) и, наконец, из вершины 2 (-1, –1, 1) в вершину 0 (-1, 1, 1). Второй треугольник, Face 1, будет нарисован с линиями от вершины 1 до вершины 2 , вершины 2 до вершины 3 и вершины 3 до вершины 1 .

Эквивалентный код будет примерно таким:

  var mesh = новый SoftEngine.  Mesh ( «Квадрат» , 4, 2); 
  eshes.Add (сетка); 
  esh.Vertices [0] = new Vector3 (-1, 1, 1); 
  esh.Vertices [1] = новый Vector3 (1, 1, 1); 
  esh.Vertices [2] = новый Vector3 (-1, -1, 1); 
  esh.Vertices [3] = новый Vector3 (1, -1, 1); 
  mesh.Faces [0] = new Face {A = 0, B = 1, C = 2}; 
  esh.Faces [1] = новое лицо {A = 1, B = 2, C = 3}; 

Если вы хотите нарисовать весь куб, вам нужно найти 10 оставшихся граней, поскольку у нас есть 12 граней для 6 сторон нашего куба.

Давайте теперь определим код для объекта Face . Это очень простой объект, так как это всего лишь набор из 3 индексов . Вот код Face и новое определение Mesh, которое также теперь использует его:

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

Теперь нам нужно обновить функцию / метод Render () нашего объекта Device, чтобы выполнить итерацию по всем определенным граням и нарисовать связанные треугольники.

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

Вот новая декларация:

  var mesh = новый SoftEngine.  Mesh ( «Куб» , 8, 12); 
  eshes.Add (сетка); 
  esh.Vertices [0] = new Vector3 (-1, 1, 1); 
  esh.Vertices [1] = новый Vector3 (1, 1, 1); 
  esh.Vertices [2] = новый Vector3 (-1, -1, 1); 
  esh.Vertices [3] = новый Vector3 (1, -1, 1); 
  esh.Vertices [4] = новый Vector3 (-1, 1, -1); 
  esh.Vertices [5] = новый Vector3 (1, 1, -1); 
  esh.Vertices [6] = новый Vector3 (1, -1, -1); 
  esh.Vertices [7] = новый Vector3 (-1, -1, -1); 
  mesh.Faces [0] = new Face {A = 0, B = 1, C = 2}; 
  esh.Faces [1] = новое лицо {A = 1, B = 2, C = 3}; 
  esh.Faces [2] = new Face {A = 1, B = 3, C = 6}; 
  esh.Faces [3] = новое лицо {A = 1, B = 5, C = 6}; 
  esh.Faces [4] = новое лицо {A = 0, B = 1, C = 4}; 
  esh.Faces [5] = новое лицо {A = 1, B = 4, C = 5}; 
  mesh.Faces [6] = new Face {A = 2, B = 3, C = 7}; 
  esh.Faces [7] = новое лицо {A = 3, B = 6, C = 7}; 
  esh.Faces [8] = новое лицо {A = 0, B = 2, C = 7}; 
  esh.Faces [9] = новое лицо {A = 0, B = 4, C = 7}; 
  esh.Faces [10] = новое лицо {A = 4, B = 5, C = 6}; 
  esh.Faces [11] = новое лицо {A = 4, B = 6, C = 7}; 

Теперь у вас должен быть этот красивый вращающийся куб:

Congrats! ?

Улучшение алгоритма рисования линий с Bresenham

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

Вот версии этого алгоритма в C #, TypeScript и JavaScript:

  public void DrawBline ( Vector2 point0, Vector2 point1) 
     int x0 = ( int ) point0.X; 
     int y0 = ( int ) point0.Y; 
     int x1 = ( int ) point1.X; 
     int y1 = ( int ) point1.Y; 
           
     var dx = Math .Abs (x1 - x0); 
     var dy = Math .Abs (y1 - y0); 
     var sx = (x0 <x1)?  1: -1; 
     var sy = (y0 <y1)?  1: -1; 
     var err = dx - dy; 
      while ( true ) { 
         DrawPoint ( новый Vector2 (x0, y0)); 
          if ((x0 == x1) && (y0 == y1)) break ; 
         var e2 = 2 * err; 
         if (e2> -dy) {err - = dy;  x0 + = sx;  } 
         if (e2 <dx) {err + = dx;  y0 + = sy;  } 
     } 
 

В функции рендеринга замените вызов do DrawLine на DrawBline, и вы должны заметить, что это немного более плавно и более резко:

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

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

C # : SoftEngineCSharpPart2.zip

TypeScript : SoftEngineTSPart2.zip

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

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

image

Увидимся в третьей части.

Первоначально опубликовано: http://blogs.msdn.com/b/davrous/archive/2013/06/14/tutorial-part-2-learning-how-to-write-a-3d-soft-engine-from-scratch -in-c-ts-or-js-drawing-lines-amp-triangles.aspx . Перепечатано здесь с разрешения автора.