Статьи

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

Вот заключительный урок этой длинной серии . Мы увидим, как применить текстуру к мешу, используя координаты сопоставления, экспортированные из Blender. Если вам удалось разобраться с предыдущими уроками, то будет просто применить некоторые текстуры. Основная идея состоит в том, чтобы еще раз интерполировать некоторые данные между каждой вершиной. Во второй части этого руководства мы увидим, как повысить производительность нашего алгоритма рендеринга. Для этого мы собираемся отображать только видимые лица, используя подход отбраковки задних лиц . Но чтобы пойти еще дальше, мы будем использовать наше последнее секретное оружие: GPU. Затем вы поймете, почему технологии OpenGL / WebGL и DirectX так важны для создания 3D-игр в реальном времени. Они помогают использовать графический процессор вместо процессора для визуализации наших 3D-объектов. Чтобы действительно увидеть различия, мы загрузим точно такой же файл JSON в 3D-движок WebGL с именем Babylon.JS . Рендеринг будет намного лучше, а FPS будет без сравнения, особенно на бюджетных устройствах!

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

Наложение текстуры

концепция

Давайте начнем с определения Википедии: Отображение текстуры : « Карта текстуры наносится (отображается) на поверхность фигуры или многоугольника. Этот процесс похож на нанесение узорной бумаги на простую белую коробку. Каждой вершине в многоугольнике назначается текстурная координата (которая в двумерном случае также известна как координата UV) либо посредством явного присваивания, либо по процедурному определению. Места выборки изображений затем интерполируются по поверхности многоугольника, чтобы получить визуальный результат, который кажется более насыщенным, чем можно было бы достичь с помощью ограниченного числа многоугольников. »

Давайте теперь попробуем понять, что это значит.

Первый раз, когда я попытался представить, как мы можем применить текстуру к трехмерной сетке, сначала подумал о кубе, первой сетке, которую мы нарисовали в этой серии. Затем я подумал о том, чтобы взять изображение, играющее роль нашей текстуры, и сопоставить его с гранями каждого куба. Это может хорошо работать в таком простом случае. Но первая проблема будет: что, если я хотел бы применить другое изображение / текстуру к граням каждого куба? Первой идеей может быть взять 6 разных изображений для 6 сторон вашего куба. Чтобы быть еще более точным, возьмите 6 изображений, разделите их на 2 треугольника, которые будут сопоставлены с 12 треугольниками куба.

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

образ

Тот же самый подход прекрасно работает с 3D движком. Представьте это изображение как текстуру, которая будет применена к нашему кубу. Рассмотрите это как 2-мерный массив байтов цвета. Мы сможем повлиять на некоторые 2D-координаты, перемещающиеся в этот массив к вершине каждого куба, чтобы получить что-то подобное:

образ

Изображение взято из этой ветки форума: текстурирование куба в Blender, и собираюсь вырвать мои волосы

Эти 2D текстурные координаты известны как UV-координаты .

Примечание: я спросил гуру 3D, каковы были причины назвать их U & V? Ответ был удивительно очевиден: « Ну, это потому, что это прямо перед X, Y, Z ». Я ожидал более сложного ответа! ?

Теперь вы, вероятно, спрашиваете себя, как обращаться с сложными сложными сетками, такими как Сюзанна, голова нашей прекрасной обезьяны, не так ли?

Для этого вида сетки мы также будем использовать одно 2D-изображение, которое будет отображаться в 3D. Чтобы построить соответствующую текстуру, нам нужен запланированный 2D вид вашей сетки. Эта операция известна как операция развертывания . Если вы плохой разработчик, как я, поверьте мне, вам понадобится блестящий 3D-дизайнер, как мой друг Мишель Руссо, чтобы помочь вам на этом этапе! И это именно то, что я сделал: попросить помощи. ?

Используя модель Сюзанны в качестве примера, после операции развертывания дизайнер получит такой результат:

образ

Затем дизайнер нарисует этот запланированный 2D-вид, и в результате получится текстура, готовая для использования нашим движком. В нашем случае Мишель Руссо сделал эту работу для нас, и вот его собственная версия Сюзанны:

образ

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

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

Урок 16 — Основы наложения текстур. Прочтите первую часть, которая поможет понять, как сопоставить координаты УФ (между 0 и 1) с треугольниками наших сеток.
Руководство по Blender 2.6 — UV Mapping Mesh , который описывает различные типы картирования
Урок 5 — Отображение текстур , прочитайте первую часть, которая определенно поможет вам хотя бы узнать, как отобразить куб. ?

Код

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

1 — Создайте класс текстуры, который будет загружать изображение в качестве текстуры и возвращать цвет, связанный с координатами U & V, интерполированными на пиксель
2 — Добавить / передать информацию о текстуре в полном процессе рендеринга
3 — проанализировать файл JSON, экспортированный дополнением Babylon Blender, для загрузки координат UV

Логика текстуры

В HTML5 с TypeScript / JavaScript мы, конечно, собираемся загрузить текстуру, динамически создавая элемент canvas и получая связанные с ним данные изображения, чтобы получить наш массив цветных байтов.

С C # / XAML мы собираемся создать WriteableBitmap, установить его источник с изображением, которое мы будем загружать, и получить его свойство PixelBuffer для получения нашего цветового массива байтов.

  Текстура общественного класса 
  { 
     закрытый байт [] internalBuffer; 
     private int width; 
     частный int высота; 
      // Работа с текстурой фиксированного размера (512x512, 1024x1024 и т. Д.). 
     публичная текстура ( строковое имя файла, int width, int height) 
     { 
         это .width = ширина; 
         это. высота = высота; 
         Загрузить (имя файла); 
     } 
      async void Load ( строка имени файла) 
     { 
         var file = await Windows.ApplicationModel.  Пакет .Current.InstalledLocation.GetFileAsync (имя файла); 
          использование ( var stream = await file.OpenReadAsync ()) 
         { 
             var bmp = new WriteableBitmap (ширина, высота); 
             bmp.SetSource (поток); 
              internalBuffer = bmp.PixelBuffer.ToArray (); 
         } 
     } 
      // Принимает координаты U & V, экспортированные Blender 
     // и возвращаем соответствующий цвет пикселя в текстуре 
     общественная карта Color4 ( float tu, float tv) 
     { 
         // Изображение еще не загружено 
         if (internalBuffer == null ) 
         { 
             вернуть Color4. White; 
         } 
         // используя оператор% для циклического повторения / повторения текстуры при необходимости 
         int u = Math .Abs (( int ) (tu * width)% width); 
         int v = Math .Abs (( int ) (tv * height)% height); 
          int pos = (u + v * width) * 4; 
         байт b = internalBuffer [pos]; 
         байт g = internalBuffer [pos + 1]; 
         байт r = внутренний буфер [pos + 2]; 
         байт a = internalBuffer [pos + 3]; 
          вернуть новый Color4 (r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f); 
     } 
 

  класс экспорта текстуры { 
     ширина: число ; 
     высота: число ; 
     internalBuffer: ImageData; 
      // Работа с текстурой фиксированного размера (512x512, 1024x1024 и т. Д.). 
     конструктор (имя файла: строка , ширина: число , высота: число ) { 
         это .width = ширина; 
         это. высота = высота; 
         this .load (имя файла); 
     } 
      публичная загрузка (имя файла: строка ): void { 
         var imageTexture = new Image (); 
         imageTexture.height = this .height; 
         imageTexture.width = это .width; 
         imageTexture.onload = () => { 
             var internalCanvas: HTMLCanvasElement = document.createElement ( "canvas" ); 
             internalCanvas.width = это .width; 
             internalCanvas.height = this .height; 
             var internalContext: CanvasRenderingContext2D = internalCanvas.getContext ( "2d" ); 
             internalContext.drawImage (imageTexture, 0, 0); 
             this .internalBuffer = internalContext.getImageData (0, 0, это .width, это .height); 
         }; 
         imageTexture.src = имя файла; 
     } 
      // Принимает координаты U & V, экспортированные Blender 
     // и возвращаем соответствующий цвет пикселя в текстуре 
     общественная карта (tu: номер , tv: номер ): BABYLON.Color4 { 
         if ( this .internalBuffer) { 
             // используя оператор% для циклического повторения / повторения текстуры при необходимости 
             var u = Math.abs (((tu * this .width)% this .width)) >> 0; 
             var v = Math.abs (((tv * this .height)% this .height)) >> 0; 
              var pos = (u + v * this .width) * 4; 
              var r = this .internalBuffer.data [pos]; 
             var g = this .internalBuffer.data [pos + 1]; 
             var b = this .internalBuffer.data [pos + 2]; 
             var a = this .internalBuffer.data [pos + 3]; 
              вернуть новый BABYLON.Color4 (r / 255.0, g / 255.0, b / 255.0, a / 255.0); 
         } 
         // Изображение еще не загружено 
         еще { 
             вернуть новый BABYLON.Color4 (1, 1, 1, 1); 
         } 
     } 
 

  var Texture = ( function () { 
     // Работа с текстурой фиксированного размера (512x512, 1024x1024 и т. Д.). 
     Текстура функции (имя файла, ширина, высота) { 
         это .width = ширина; 
         это. высота = высота; 
         this .load (имя файла); 
     } 
      Texture.prototype.load = function (filename) { 
         var _this = this ; 
         var imageTexture = new Image (); 
         imageTexture.height = this .height; 
         imageTexture.width = это .width; 
         imageTexture.onload = function () { 
             var internalCanvas = document.createElement ( "canvas" ); 
             internalCanvas.width = _this.width; 
             internalCanvas.height = _this.height; 
             var internalContext = internalCanvas.getContext ( "2d" ); 
             internalContext.drawImage (imageTexture, 0, 0); 
             _this.internalBuffer = internalContext.getImageData (0, 0, _this.width, _this.height); 
         }; 
         imageTexture.src = имя файла; 
     }; 
      // Принимает координаты U & V, экспортированные Blender 
     // и возвращаем соответствующий цвет пикселя в текстуре 
     Texture.prototype.map = function (tu, tv) { 
         if ( this .internalBuffer) { 
             // используя оператор% для циклического повторения / повторения текстуры при необходимости 
             var u = Math.abs (((tu * this .width)% this .width)) >> 0; 
             var v = Math.abs (((tv * this .height)% this .height)) >> 0; 
              var pos = (u + v * this .width) * 4; 
              var r = this .internalBuffer.data [pos]; 
             var g = this .internalBuffer.data [pos + 1]; 
             var b = this .internalBuffer.data [pos + 2]; 
             var a = this .internalBuffer.data [pos + 3]; 
              вернуть новый BABYLON.Color4 (r / 255.0, g / 255.0, b / 255.0, a / 255.0); 
         } 
         // Изображение еще не загружено 
         еще { 
             вернуть новый BABYLON.Color4 (1, 1, 1, 1); 
         } 
     }; 
     вернуть текстуру; 
  ) (); 
  oftEngine.Texture = Texture; 

Передать информацию о текстуре в потоке

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

— добавить свойство Texture в класс Mesh и свойство Vector2 с именем TextureCoordinates в структуру Vertex.

— обновить ScanLineData, чтобы добавить еще 8 чисел с плавающей запятой: UV-координаты на вершину (ua, ub, uc, ud & va, vb, vc, vd).

— обновить метод / функцию Project, чтобы она возвращала новую вершину с координатами TextureCoordinates, переданными как есть (через)

— передать объект текстуры в качестве последнего параметра в методы / функции ProcessScanLine , DrawTriangle

— Заполните новую структуру ScanLineData в drawTriangle соответствующими координатами UV

Интерполируйте UV в ProcessScanLine на Y, чтобы получить SU / SV и EU / EV (начало U / начало V / End U / End V), затем интерполируйте U, V на X, найдите соответствующий цвет в текстуре. Эта цветная текстура будет смешана с цветом нативного объекта (всегда белым в нашем случае) и количеством света, измеренным с помощью операции NDotL с нормальным.

Примечание: наш метод Project можно рассматривать как то, что мы называем « Vertex Shader » в трехмерном аппаратном движке, а наш ProcessScanLine можно рассматривать как « Pixel Shader ».

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

  void ProcessScanLine ( данные ScanLineData , вершина va, вершина vb, вершина vc, вершина vd, цвет Color4 , текстура текстуры) 
     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); 
      // интерполируем нормали по Y 
     var snl = Interpolate (data.ndotla, data.ndotlb, градиент1); 
     var inv = Interpolate (data.ndotlc, data.ndotld, градиент2); 
      // Интерполяция координат текстуры по Y 
     var su = Interpolate (data.ua, data.ub, градиент1); 
     var eu = Interpolate (data.uc, data.ud, градиент2); 
     var sv = Interpolate (data.va, data.vb, градиент1); 
     var ev = Interpolate (data.vc, data.vd, градиент2); 
      // рисуем линию слева (sx) вправо (ex)  
     для ( var x = sx; x <ex; x ++) 
     { 
         градиент плавания = (x - sx) / ( плавание ) (ex - sx); 
          // интерполируем координаты Z, нормали и текстуры по X 
         var z = интерполировать (z1, z2, градиент); 
         var ndotl = интерполировать (snl, inv, градиент); 
         var u = интерполировать (su, ес, градиент); 
         var v = интерполировать (sv, ev, градиент); 
          Color4 textureColor; 
          если (текстура! = ноль ) 
             textureColor = texture.Map (u, v); 
         еще 
             textureColor = new Color4 (1, 1, 1, 1); 
          // изменение значения собственного цвета с использованием косинуса угла 
         // между вектором света и вектором нормали 
         // и цвет текстуры 
         DrawPoint ( новый Vector3 (x, data.currentY, z), цвет * ndotl * textureColor); 
     } 
 

  public processScanLine (данные: ScanLineData, va: вершина, vb: вершина, 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); 
      // интерполируем нормали по Y 
     var snl = this .interpolate (data.ndotla, data.ndotlb, градиент1); 
     var inv = this .interpolate (data.ndotlc, data.ndotld, градиент2); 
      // Интерполяция координат текстуры по Y 
     var su = this .interpolate (data.ua, data.ub, градиент1); 
     var eu = this .interpolate (data.uc, data.ud, градиент2); 
     var sv = this .interpolate (data.va, data.vb, градиент1); 
     var ev = this .interpolate (data.vc, data.vd, градиент2); 
      // рисуем линию слева (sx) вправо (ex)  
     для ( var x = sx; x <ex; x ++) { 
         градиент var : число = (x - sx) / (ex - sx); 
          // интерполируем координаты Z, нормали и текстуры по X 
         var z = this .interpolate (z1, z2, Градиент); 
         var ndotl = this .interpolate (snl, inv, градиент); 
         var u = this .interpolate (su, ес, градиент); 
         var v = this .interpolate (sv, ev, градиент); 
          var textureColor; 
          если (текстура) 
             textureColor = texture.map (u, v); 
         еще 
             textureColor = new BABYLON.Color4 (1, 1, 1, 1); 
          // изменение значения собственного цвета с использованием косинуса угла 
         // между вектором света и вектором нормали 
         // и цвет текстуры 
         this .drawPoint ( новый BABYLON.Vector3 (x, data.currentY, z), 
новый BABYLON.Color4 (color.r * ndotl * textureColor.r,
color.g * ndotl * textureColor.g,
color.b * ndotl * textureColor.b, 1)); }

  Device.prototype.processScanLine = function (data, va, vb, vc, vd, color, texture) { 
     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); 
      // интерполируем нормали по Y 
     var snl = this .interpolate (data.ndotla, data.ndotlb, градиент1); 
     var inv = this .interpolate (data.ndotlc, data.ndotld, градиент2); 
      // Интерполяция координат текстуры по Y 
     var su = this .interpolate (data.ua, data.ub, градиент1); 
     var eu = this .interpolate (data.uc, data.ud, градиент2); 
     var sv = this .interpolate (data.va, data.vb, градиент1); 
     var ev = this .interpolate (data.vc, data.vd, градиент2); 
      // рисуем линию слева (sx) вправо (ex)  
     для ( var x = sx; x <ex; x ++) { 
         градиент var = (x - sx) / (ex - sx); 
          // интерполируем координаты Z, нормали и текстуры по X 
         var z = this .interpolate (z1, z2, Градиент); 
         var ndotl = this .interpolate (snl, inv, градиент); 
         var u = this .interpolate (su, ес, градиент); 
         var v = this .interpolate (sv, ev, градиент); 
          var textureColor; 
          если (текстура) 
             textureColor = texture.map (u, v); 
         еще 
             textureColor = new BABYLON.Color4 (1, 1, 1, 1); 
          // изменение значения собственного цвета с использованием косинуса угла 
         // между вектором света и вектором нормали 
         // и цвет текстуры 
         this .drawPoint ( новый BABYLON.Vector3 (x, data.currentY, z), 
новый BABYLON.Color4 (color.r * ndotl * textureColor.r,
color.g * ndotl * textureColor.g,
color.b * ndotl * textureColor.b, 1)); } ;

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

Загрузка информации из формата файла Babylon JSON

Чтобы иметь хороший рендеринг, который вы видели в начале этой статьи, вам необходимо загрузить новую версию Suzanne, модифицированную Мишелем Руссо и экспортированную из Blender с UV-координатами. Для этого, пожалуйста, скачайте эти 2 файла:

— Модель Сюзанны Блендер с набором UV-координат: http://david.blob.core.windows.net/softengine3d/part6/monkey.babylon

— изображение текстуры 512 × 512 для загрузки: http://david.blob.core.windows.net/softengine3d/part6/Suzanne.jpg

Файл Дэвида Катухе в формате Babylon.JSON содержит много деталей, которые мы не рассмотрим в этой серии. Например, вам может быть интересно поиграть с материалом. Действительно, дизайнер может назначить конкретный материал для сетки. В нашем случае мы будем обрабатывать только диффузную текстуру. Если вы хотите реализовать больше из них, взгляните на статью Дэвида Катухе в качестве основы: Babylon.js: опубликуйте StandardMaterial для вашей игры babylon.js

Опять же, я делюсь с вами только основной частью, которую нужно изменить: загрузкой метода / функции и анализом файла JSON.

  // Загрузка файла JSON в асинхронном режиме 
  public Async Task < Mesh []> LoadJSONFileAsync ( строка fileName) 
     var meshes = new List < Mesh > (); 
     var материалы = новый словарь < String , Material > (); 
     var file = await Windows.ApplicationModel.  Package .Current.InstalledLocation.GetFileAsync (fileName); 
     var data = ожидание Windows.Storage.  FileIO .ReadTextAsync (файл); 
     динамический jsonObject = Newtonsoft.Json.  JsonConvert .DeserializeObject (data); 
      for ( var materialIndex = 0; materialIndex <jsonObject.materials.Count; materialIndex ++) 
     { 
         var материал = новый материал (); 
         material.Name = jsonObject.materials [materialIndex] .name.Value; 
         material.ID = jsonObject.materials [materialIndex] .id.Value; 
         if (jsonObject.materials [materialIndex] .diffuseTexture! = null ) 
             material.DiffuseTextureName = jsonObject.materials [materialIndex] .diffuseTexture.name.Value; 
          материалы. Добавить (материал. ID, материал); 
     } 
      для ( var meshIndex = 0; meshIndex <jsonObject.meshes.Count; meshIndex ++) 
     { 
         var verticesArray = jsonObject.meshes [meshIndex] .vertices; 
         // Лица 
         var indicesArray = jsonObject.meshes [meshIndex] .indices; 
          var uvCount = jsonObject.meshes [meshIndex] .uvCount.Value; 
         var verticesStep = 1; 
          // В зависимости от количества координат текстуры на вершину 
         // прыгаем в массиве вершин по 6, 8 и 10 рамкам окна 
         switch (( int ) uvCount) 
         { 
             случай 0: 
                 verticesStep = 6; 
                 перерыв ; 
             случай 1: 
                 verticesStep = 8; 
                 перерыв ; 
             случай 2: 
                 verticesStep = 10; 
                 перерыв ; 
         } 
          // количество интересных вершин информации для нас 
         var verticesCount = verticesArray.Count / verticesStep; 
         // количество граней - это логически размер массива, деленный на 3 (A, B, C) 
         varfaceCount = indicesArray.Count / 3; 
         var mesh = new Mesh (jsonObject.meshes [meshIndex] .name.Value, verticesCount, FaceCount); 
          // Сначала заполняем массив вершин нашей сетки 
         для ( 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] = новая вершина 
             { 
                 Координаты = новый вектор3 (x, y, z), 
                 Normal = new Vector3 (nx, ny, nz) 
             }; 
              if (uvCount> 0) 
             { 
                 // Загрузка текстурных координат 
                 float u = ( float ) verticesArray [index * verticesStep + 6] .Value; 
                 float v = ( float ) verticesArray [index * verticesStep + 7] .Value; 
                 mesh.Vertices [index] .TextureCoordinates = new Vector2 (u, v); 
             } 
         } 
          // Затем заполняем массив Faces 
         для ( var index = 0; index <FaceCount; index ++) 
         { 
             var a = ( int ) indicesArray [index * 3] .Value; 
             var b = ( int ) indicesArray [index * 3 + 1] .Value; 
             var c = ( int ) indicesArray [index * 3 + 2] .Value; 
             mesh.Faces [index] = new Face {A = a, B = b, C = c}; 
         } 
          // Получение позиции, которую вы установили в Blender 
         var position = jsonObject.meshes [meshIndex] .position; 
         mesh.Position = new Vector3 (( float ) position [0] .Value, ( float ) position [1] .Value, ( float ) position [2] .Value); 
          if (uvCount> 0) 
         { 
             // Текстура 
             var meshTextureID = jsonObject.meshes [meshIndex] .materialId.Value; 
             var meshTextureName = materials [meshTextureID] .DiffuseTextureName; 
             mesh.Texture = новая текстура (meshTextureName, 512, 512); 
         } 
          meshes.Add (сетка); 
     } 
     return meshes.ToArray (); 
 

  private CreateMeshesFromJSON (jsonObject): Mesh [] { 
     var meshes: Mesh [] = []; 
     Var материалы: Материал [] = []; 
      for ( var materialIndex = 0; materialIndex <jsonObject.materials.length; materialIndex ++) { 
         var материал: Material = {}; 
          material.Name = jsonObject.materials [materialIndex] .name; 
         material.ID = jsonObject.materials [materialIndex] .id; 
         if (jsonObject.materials [materialIndex] .diffuseTexture) 
             material.DiffuseTextureName = jsonObject.materials [materialIndex] .diffuseTexture.name; 
          материалы [материал.ID] = материал; 
     } 
      для ( var meshIndex = 0; meshIndex <jsonObject.meshes.length; meshIndex ++) { 
         var verticesArray: number [] = jsonObject.meshes [meshIndex] .vertices; 
         // Лица 
         var indicesArray: number [] = jsonObject.meshes [meshIndex] .indices; 
          var uvCount: number = jsonObject.meshes [meshIndex] .uvCount; 
         var verticesStep = 1; 
          // В зависимости от количества координат текстуры на вершину 
         // прыгаем в массиве вершин по 6, 8 и 10 рамкам окна 
         switch (uvCount) { 
             случай 0: 
                 verticesStep = 6; 
                 перерыв ; 
             случай 1: 
                 verticesStep = 8; 
                 перерыв ; 
             случай 2: 
                 verticesStep = 10; 
                 перерыв ; 
         } 
          // количество интересных вершин информации для нас 
         var verticesCount = verticesArray.length / verticesStep; 
         // количество граней - это логически размер массива, деленный на 3 (A, B, C) 
         var faceCount = indicesArray.length / 3; 
         var mesh = new SoftEngine.Mesh (jsonObject.meshes [meshIndex] .name, verticesCount, FaceCount); 
               
         // Сначала заполняем массив вершин нашей сетки 
         for ( var index = 0; index <verticesCount; index ++) { 
             var x = verticesArray [index * verticesStep]; 
             var y = verticesArray [index * verticesStep + 1]; 
             var z = verticesArray [index * verticesStep + 2]; 
             // Загрузка нормали вершины, экспортируемой Blender 
             var nx = verticesArray [index * verticesStep + 3]; 
             var ny = verticesArray [index * verticesStep + 4]; 
             var nz = verticesArray [index * verticesStep + 5]; 
              mesh.Vertices [index] = { 
                 Координаты: новый BABYLON.Vector3 (x, y, z), 
                 Нормальный: новый BABYLON.Vector3 (nx, ny, nz) 
             }; 
              if (uvCount> 0) { 
                 // Загрузка текстурных координат 
                 var u = verticesArray [index * verticesStep + 6]; 
                 var v = verticesArray [index * verticesStep + 7]; 
                 mesh.Vertices [index] .TextureCoordinates = new BABYLON.Vector2 (u, v); 
             } 
             еще { 
                 mesh.Vertices [index] .TextureCoordinates = new BABYLON.Vector2 (0, 0); 
             } 
         } 
               
         // Затем заполняем массив Faces 
         for ( var index = 0; index <FaceCount; index ++) { 
             var a = indicesArray [index * 3]; 
             var b = indicesArray [index * 3 + 1]; 
             var c = indicesArray [index * 3 + 2]; 
             mesh.Faces [index] = { 
                 A: a, 
                 B: B, 
                 C: C 
             }; 
         } 
               
         // Получение позиции, которую вы установили в Blender 
         var position = jsonObject.meshes [meshIndex] .position; 
         mesh.Position = new BABYLON.Vector3 (position [0], position [1], position [2]); 
          if (uvCount> 0) { 
             var meshTextureID = jsonObject.meshes [meshIndex] .materialId; 
             var meshTextureName = materials [meshTextureID] .DiffuseTextureName; 
             mesh.Texture = новая текстура (meshTextureName, 512, 512); 
         } 
          meshes.push (сетка); 
     } 
     возвратные сетки;  
 

  Device.prototype.CreateMeshesFromJSON = function (jsonObject) { 
     var meshes = []; 
     Var материалы = []; 
      for ( var materialIndex = 0; materialIndex <jsonObject.materials.length; materialIndex ++) { 
         var material = {}; 
          material.Name = jsonObject.materials [materialIndex] .name; 
         material.ID = jsonObject.materials [materialIndex] .id; 
         if (jsonObject.materials [materialIndex] .diffuseTexture) 
             material.DiffuseTextureName = jsonObject.materials [materialIndex] .diffuseTexture.name; 
          материалы [материал.ID] = материал; 
     } 
      для ( var meshIndex = 0; meshIndex <jsonObject.meshes.length; meshIndex ++) { 
         var verticesArray = jsonObject.meshes [meshIndex] .vertices; 
         // Лица 
         var indicesArray = jsonObject.meshes [meshIndex] .indices; 
          var uvCount = jsonObject.meshes [meshIndex] .uvCount; 
         var verticesStep = 1; 
          // В зависимости от количества координат текстуры на вершину 
         // прыгаем в массиве вершин по 6, 8 и 10 рамкам окна 
         switch (uvCount) { 
             случай 0: 
                 verticesStep = 6; 
                 перерыв ; 
             случай 1: 
                 verticesStep = 8; 
                 перерыв ; 
             случай 2: 
                 verticesStep = 10; 
                 перерыв ; 
         } 
          // количество интересных вершин информации для нас 
         var verticesCount = verticesArray.length / verticesStep; 
         // количество граней - это логически размер массива, деленный на 3 (A, B, C) 
         var faceCount = indicesArray.length / 3; 
         var mesh = new SoftEngine.Mesh (jsonObject.meshes [meshIndex] .name, verticesCount, FaceCount); 
          // Сначала заполняем массив вершин нашей сетки 
         for ( var index = 0; index <verticesCount; index ++) { 
             var x = verticesArray [index * verticesStep]; 
             var y = verticesArray [index * verticesStep + 1]; 
             var z = verticesArray [index * verticesStep + 2]; 
             // Загрузка нормали вершины, экспортируемой Blender 
             var nx = verticesArray [index * verticesStep + 3]; 
             var ny = verticesArray [index * verticesStep + 4]; 
             var nz = verticesArray [index * verticesStep + 5]; 
              mesh.Vertices [index] = { 
                 Координаты: новый BABYLON.Vector3 (x, y, z), 
                 Нормальный: новый BABYLON.Vector3 (nx, ny, nz) 
             }; 
              if (uvCount> 0) { 
                 // Загрузка текстурных координат 
                 var u = verticesArray [index * verticesStep + 6]; 
                 var v = verticesArray [index * verticesStep + 7]; 
                 mesh.Vertices [index] .TextureCoordinates = new BABYLON.Vector2 (u, v); 
             } 
             еще { 
                 mesh.Vertices [index] .TextureCoordinates = new BABYLON.Vector2 (0, 0); 
             } 
         } 
          // Затем заполняем массив Faces 
         for ( var index = 0; index <FaceCount; index ++) { 
             var a = indicesArray [index * 3]; 
             var b = indicesArray [index * 3 + 1]; 
             var c = indicesArray [index * 3 + 2]; 
             mesh.Faces [index] = { 
                 A: a, 
                 B: B, 
                 C: C 
             }; 
         } 
          // Получение позиции, которую вы установили в Blender 
         var position = jsonObject.meshes [meshIndex] .position; 
         mesh.Position = new BABYLON.Vector3 (position [0], position [1], position [2]); 
          if (uvCount> 0) { 
             var meshTextureID = jsonObject.meshes [meshIndex] .materialId; 
             var meshTextureName = materials [meshTextureID] .DiffuseTextureName; 
             mesh.Texture = новая текстура (meshTextureName, 512, 512); 
         } 
          meshes.push (сетка); 
     } 
     возвратные сетки; 
  ; 

Благодаря всем этим модификациям у нас теперь есть прекрасный рендеринг, на котором Сюзанна текстурирована с помощью алгоритма затенения Гуро:

образ

Программный 3D-движок: в вашем браузере можно увидеть текстуру Сюзанны с затенением Гуро в HTML5

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

C # : SoftEngineCSharpPart6Sample1.zip

TypeScript : SoftEngineTSPart6Sample1.zip

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

Производительность не огромная. Я использую версию C # в разрешении 1600 × 900 со скоростью 18 кадров в секунду на моей машине и версию HTML5 со скоростью 640 × 480 со скоростью 15 кадров в секунду в IE11.

Но прежде чем обратиться за помощью к графическому процессору, давайте посмотрим на окончательную оптимизацию вашего программного движка 3D.

Отбраковка

Давайте начнем снова с чтения определения из Википедии: Отбор на задней стороне : « В компьютерной графике отбор на задней стороне определяет, является ли полигон графического объекта видимым <…> Один из способов реализации отбраковки на задней стороне состоит в отбрасывании всех полигонов. где скалярное произведение их поверхности нормали и вектора от камеры к многоугольнику больше или равно нулю ».

В нашем случае идея состоит в том, чтобы предварительно вычислить каждую нормаль поверхности сетки на этапе загрузки JSON, используя тот же алгоритм, который использовался в предыдущем уроке для плоского затенения. После этого в методе / функции Render мы преобразуем координаты нормали поверхности в вид на мир (мир, видимый камерой) и проверим его значение Z. Если это> = 0, мы вообще не будем рисовать треугольник, так как это означает, что эта грань не видна с точки зрения камеры.

Программный 3D-движок: просмотр текстуры Сюзанны с затенением Гуро в HTML5 с включенной выборкой на оборотной стороне

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

C # : SoftEngineCSharpPart6Sample2.zip

TypeScript : SoftEngineTSPart6Sample2.zip

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

Примечание: вы заметите, что у меня есть небольшая ошибка рендеринга в моей реализации отбраковки задней поверхности. Очень немногие треугольники не нарисованы, тогда как они должны быть. Это потому, что мы должны отрегулировать преобразование нормали, чтобы учесть текущую перспективу камеры. Текущий алгоритм предполагает, что у нас есть ортогональная камера, а это не так. Это может быть хорошим упражнением для вас, чтобы это исправить! ?

Повышение производительности интересно и составляет около 66%, так как я переключаюсь со среднего значения 15 кадров в секунду в IE11 на 25 кадров в секунду с включенной выборкой с обратной стороны.

Рендеринг с WebGL благодаря Babylon.JS

Современные современные 3D-игры, конечно, используют графический процессор. Целью этой серии было действительно понять основы 3D, создав собственный программный движок 3D. После того, как вы сможете понять 6 частей серии, вам будет гораздо проще перейти к трехмерному движку с использованием OpenGL / WebGL или DirectX.

Со своей стороны, мы работаем над набором фреймворков во Франции, чтобы позволить разработчикам создавать HTML5-игры 3D очень простым способом. Первым шагом был выпуск Babylon.JS, созданный Дэвидом Катухе. Но мы работаем над другими классными фреймворками поверх его потрясающего 3D-движка, чтобы помочь вам создавать ваши игры WebGL.

Дэвид начал серию обучающих программ в своем блоге о том, как использовать свой движок 3D WebGL. Точка входа здесь: Babylon.js: полный JavaScript-фреймворк для создания 3D-игр с HTML 5 и WebGL.

Взяв этот учебник: Babylon.js: Как загрузить файл .babylon, созданный с помощью Blender , вы сможете перезагрузить нашу сетку, использованную в этой серии, и воспользоваться преимуществами аппаратного ускорения графического процессора в браузере!

Если у вас есть IE11, Chrome или Firefox или какое-либо WebGL-совместимое устройство / браузер, вы можете проверить результат здесь:

образ

Babylon.JS — 3D движок WebGL: просмотр текстур Сюзанны и аппаратного ускорения!

Благодаря WebGL у нас огромный прирост производительности. Например, в моем Surface RT, обновленном в предварительном просмотре Windows 8.1 с использованием IE11, я переключаюсь с менее чем 4 кадров в секунду при 640 × 480 с моим программным 3D-движком на 60 кадров в секунду при 1366 × 768 !

Эта серия сейчас закончена. Мне было очень приятно это написать. Я получил много удивительных отзывов, и некоторые из вас портировали серию на Java ( Янник Комте ), на Windows CE и в WPF! Я так рад видеть, что это было полезно для некоторых из вас и обнаруживать вилки кода. Не стесняйтесь поделиться своей версией в комментариях.

Вскоре я напишу новую серию руководств по фреймворку, над которым мы сейчас работаем над созданием 3D-игр. Будьте на связи!

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