Статьи

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

В предыдущем уроке мы научились рисовать линии и треугольники, и мы действительно начали видеть 3D-стороны наших сеток благодаря рендерингу каркаса. Но мы только показали куб … И даже простой куб уже имеет 12 граней! Будем ли мы вынуждены обрабатывать все лица более сложными объектами таким образом? Надеюсь, что нет.

3D моделисты помогают сотрудничеству между 3D дизайнерами и разработчиками . Дизайнер может использовать свои любимые инструменты для создания своих сцен или сеток (3D Studio Max, Maya, Blender и т. Д.). Затем он экспортирует свою работу в файл, который будет загружен разработчиками. Разработчики наконец-то вставят сетки в его 3D-движок реального времени. На рынке доступно несколько форматов файлов для сериализации работы, выполненной художниками. В нашем случае мы будем использовать JSON. Действительно, Дэвид Катухе создал библиотеку экспорта для Blender, которая выводит файл .babylon с использованием JSON . Затем мы увидим, как анализировать этот файл и отображать меши в нашем прекрасном мягком движке.

Blender — бесплатный 3D-моделлер, который вы можете скачать здесь: http://www.blender.org/download/get-blender/

Вы можете написать плагины в Python. Вот что мы сделали для экспортера.

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

И вы увидите, что вы уже выполнили большую часть работы в 2 предыдущих уроках, чтобы сделать это.

Установите экспортер Babylon и создайте свою собственную сцену с помощью Blender

После того, как вы установите Blender, пожалуйста, скачайте наш экспортер Babylon отсюда: io_export_babylon.py

Скопируйте этот файл в каталог \ script \ addons, где вы установили Blender (например, « C: \ Program Files \ Blender Foundation \ Blender \ 2.67 \ scripts \ addons » в моем конкретном случае).

Вам нужно активировать наш плагин в настройках пользователя. Перейдите в « Файл » -> « Пользовательские настройки » и вкладку « Дополнения ». Найдите слово « Вавилон » и активируйте его, проверив регистр.

image

Делай что хочешь с Блендером. Если вы, как и я, очень плохо умеете создавать 3D-сетки, вот классный вариант, который поразит ваших друзей на вечеринках гиков: « Добавить » -> « Сетка » -> « Обезьяна »:

image

Затем вы должны получить такой экран:

image

Последний шаг — экспорт в формат файла .babylon (наш файл JSON). « Файл » -> « Экспорт » -> « Babylon.js »

image

Назовите файл « monkey.babylon ».

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

Загрузка экспортированного файла JSON и отображение его сеток

Как я уже говорил вам в начале этой статьи, мы уже создали всю необходимую логику для отображения более сложных сеток, таких как Сюзанна. У нас есть логика Face, Mesh & Vertex. Это все, что нам нужно на данный момент.

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

Примечание: для разработчиков на C # вам нужно установить Json.NET из Newtonsoft через nuGet, как мы делали в первом уроке по добавлению SharpDX. Действительно, разбор JSON изначально не поддерживается в .NET, как в браузере с использованием JavaScript.

Давайте начнем с добавления логики загрузки внутри объекта Device :

  // Загрузка файла JSON в асинхронном режиме 
  public Async Task < Mesh []> LoadJSONFileAsync ( строка fileName) 
     var meshes = new List < Mesh > (); 
     var file = await Windows.ApplicationModel.  Package .Current.InstalledLocation.GetFileAsync (fileName); 
     var data = ожидание Windows.Storage.  FileIO .ReadTextAsync (файл); 
     динамический jsonObject = Newtonsoft.Json.  JsonConvert .DeserializeObject (data); 
      для ( 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; 
             mesh.Vertices [index] = new Vector3 (x, y, z); 
         } 
          // Затем заполняем массив 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); 
         meshes.Add (сетка); 
     } 
     return meshes.ToArray (); 
 

  // Загрузка файла JSON асинхронным способом и 
  / обратный вызов с переданной функцией, предоставляющей массив загруженных мешей 
  public LoadJSONFileAsync (имя-файла: строка , обратный вызов: (результат: Mesh []) => любой ): void { 
     var jsonObject = {}; 
     var xmlhttp = new XMLHttpRequest (); 
     xmlhttp.open ( "GET" , fileName, true ); 
     вар то = это ; 
     xmlhttp.onreadystatechange = function () { 
         if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { 
             jsonObject = JSON.parse (xmlhttp.responseText); 
             Обратный вызов (that.CreateMeshesFromJSON (JSONObject)); 
         } 
     }; 
     xmlhttp.send ( null ); 
  private CreateMeshesFromJSON (jsonObject): Mesh [] { 
     var meshes: Mesh [] = []; 
     для ( 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]; 
             mesh.Vertices [index] = новый BABYLON.Vector3 (x, y, z); 
         } 
               
         // Затем заполняем массив 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]); 
         meshes.push (сетка); 
     } 
     возвратные сетки; 
 

  // Загрузка файла JSON асинхронным способом и 
  / обратный вызов с переданной функцией, предоставляющей массив загруженных мешей 
  Device.prototype.LoadJSONFileAsync = function (fileName, callback) { 
     var jsonObject = {}; 
     var xmlhttp = new XMLHttpRequest (); 
     xmlhttp.open ( "GET" , fileName, true ); 
     вар то = это ; 
     xmlhttp.onreadystatechange = function () { 
         if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { 
             jsonObject = JSON.parse (xmlhttp.responseText); 
             Обратный вызов (that.CreateMeshesFromJSON (JSONObject)); 
         } 
     }; 
     xmlhttp.send ( null ); 
  ; 
  evice.prototype.CreateMeshesFromJSON = function (jsonObject) { 
     var meshes = []; 
     для ( 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]; 
             mesh.Vertices [index] = новый BABYLON.Vector3 (x, y, z); 
         } 
          // Затем заполняем массив 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]); 
         meshes.push (сетка); 
     } 
     возвратные сетки; 
  ; 

Вы, вероятно, удивитесь, почему мы прыгаем на 6, 8 и 10 в массиве вершин, чтобы взять нашу трехмерную координату (X, Y, Z) наших вершин. Опять же, это потому, что экспортер Babylon добавляет больше деталей, которые нам нужны для рендеринга каркаса. Вот почему мы фильтруем эти детали, используя этот подход. Эта логика специфична для нашего формата файлов. Если вы хотите загрузить экспорт из другого файла (например, из файла three.js), вам просто нужно будет указать, где получить индексы вершин и граней в другом формате файла.

Примечание: чтобы иметь возможность загружать наши файлы .babylon, разработчики TypeScript / JavaScript , вам нужно определить новый тип MIME « application / babylon », нацеленный на расширение «.babylon». В IIS вы должны объявить это внутри вашего web.config :

  < system.webServer > 
     < staticContent > 
       < mimeMap fileExtension = " .babylon " mimeType = " application / babylon " /> 
     </ staticContent > 
   </ system.webServer > 

Разработчики C # , вам нужно изменить свойства файла, который вы будете включать в решение. Переключите « Build Action » на « Content » и всегда копируйте в выходной каталог:

image

В противном случае файл не будет найден.

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

  личное устройство устройства; 
  Mesh [] сетки; 
  Camera mera = новая камера (); 
  частная асинхронная пустота Page_Loaded ( отправитель объекта , RoutedEventArgs e) 
     // Выберите разрешение заднего буфера здесь 
     WriteableBitmap bmp = new WriteableBitmap (640, 480); 
      // Наш XAML элемент управления Image 
     frontBuffer.Source = bmp; 
           
     устройство = новое устройство (bmp); 
     meshes = await device.LoadJSONFileAsync ( "monkey.babylon" ); 
     mera.Position = new Vector3 (0, 0, 10.0f); 
     mera.Target = Vector3 .Zero; 
      // Регистрация в цикле рендеринга XAML 
     CompositionTarget .Rendering + = CompositionTarget_Rendering; 
  // Визуализация обработчика цикла 
  void CompositionTarget_Rendering ( отправитель объекта , объект e) 
     device.Clear (0, 0, 0, 255); 
      foreach ( переменная сетка в сетках) { 
         // слегка поворачиваем сетки во время каждого отрисованного кадра 
         mesh.Rotation = new Vector3 (mesh.Rotation.X + 0.01f, mesh.Rotation.Y + 0.01f, mesh.Rotation.Z); 
     } 
      // Выполнение различных матричных операций 
     device.Render (мера, сетки); 
     // Сброс заднего буфера в передний буфер 
     device.Present (); 
 

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

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

C # : SoftEngineCSharpPart3.zip

TypeScript : SoftEngineTSPart3.zip

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

Итак, что теперь дальше? Ну, нам нужно заполнить треугольники . Это называется растеризацией . Мы также будем обрабатывать то, что мы называем Z-Buffer, для правильного рендеринга. В следующем уроке вы узнаете, как получить нечто подобное:

image

Мы будем заполнять треугольники случайным цветом. Увидимся в четвертом уроке.

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