Статьи

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

Я хотел бы поделиться с вами тем, как я научился создавать так называемый « трехмерный программный движок » с помощью серии учебных пособий. «Программный движок» означает, что мы будем использовать только ЦП для создания трехмерного движка в старом стиле (помните Doom на вашем 80386?).

Я поделюсь с вами версиями кода на C #, TypeScript и JavaScript . В этом списке вы должны найти свой любимый язык или, по крайней мере, что-нибудь рядом с вашим любимым языком. Идея состоит в том, чтобы помочь вам перенести следующие примеры и концепции на вашу любимую платформу. В конце вы также найдете решения Visual Studio 2012 C # / TS / JS для загрузки.

Так зачем создавать 3D-движок? Это просто потому, что это действительно помогает понять, как современные 3D работают с нашими графическими процессорами. Действительно, в настоящее время я изучаю основы 3D благодаря внутренним семинарам, проведенным в Microsoft замечательным Дэвидом Катухе . Он уже много лет осваивает 3D, и операции с матрицами жестко запрограммированы в его мозгу. Когда я был молодым, я мечтал написать такие движки, но у меня было чувство, что это слишком сложно для меня. Наконец, вы увидите, что это не так уж сложно. Вам просто нужен кто-то, кто поможет вам понять основные принципы простым способом.

Из этой серии вы узнаете, как проецировать некоторые трехмерные координаты (X, Y, Z), связанные с точкой (вершиной) на 2D-экране, как рисовать линии между каждой точкой, как заполнять несколько треугольников, обрабатывать источники света. , материалы и тд. Этот первый урок просто покажет вам, как отображать 8 точек, связанных с кубом, и как перемещать их в виртуальном трехмерном мире.

Этот учебник является частью следующих серий:

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

Если вы читаете всю серию, вы узнаете, как создать свой собственный 3D программный движок ! Затем ваш движок запустится, выполнив каркасный рендеринг, затем растеризацию, затем затенение Гуро и, наконец, применение текстур:

3dSoftEngineProgression

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

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

Отказ от ответственности: некоторые из вас задаются вопросом, почему я создаю этот 3D-движок, а не использую графический процессор. Это действительно для образовательных целей. Конечно, если вам нужно создать игру с плавной трехмерной анимацией, вам понадобится DirectX или OpenGL / WebGL. Но как только вы поймете, как создать трехмерный программный движок, вам будет проще понять более «сложный» движок. Чтобы идти дальше, вам определенно стоит взглянуть на движок BabylonJS WebGL, созданный Дэвидом Катухе. Подробнее и руководства здесь: Babylon.js: полный JavaScript-фреймворк для создания 3D-игр с HTML 5 и WebGL.

Чтение предпосылок

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

Представлена ​​матрица мира, вида и проекции
Урок 3: Матрицы , которые предоставят вам введение в матрицы, модель, вид и матрицы проекций.
Камеры на OpenGL ES 2.x — Матрица ModelViewProjection : эта действительно очень интересна, поскольку объясняет историю, начиная с того, как работают камеры и объективы.
Преобразования (Direct3D 9)
Краткое введение в 3D : отличная колода слайдов PowerPoint! Прочитайте, по крайней мере, до слайда 27. После этого это слишком связано с технологией, говорящей с GPU (OpenGL или DirectX).
Преобразование OpenGL

Преобразование вершин OpenGL

Прочитайте эти статьи, не сосредотачиваясь на связанных технологиях (таких как OpenGL или DirectX) или на концепции треугольников, которые вы, возможно, видели на рисунках. Мы увидим это позже.

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

— мы начинаем с трехмерного объекта, сосредоточенного на себе
— тот же объект затем перемещается в виртуальный трехмерный мир с помощью операций перемещения , масштабирования или вращения с помощью матриц
камера будет смотреть на этот трехмерный объект, расположенный в трехмерном мире
— окончательная проекция всего, что будет сделано в 2D-пространство, которое является вашим экраном

OpenGL Perspective Frustum и НДЦ

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

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

Затем мы будем использовать библиотеки, которые будут выполнять эту работу за нас: SharpDX , управляемая оболочка поверх DirectX, для разработчиков на C # и babylon.math.js, написанная Дэвидом Катухе для разработчиков на JavaScript. Я переписал это также на TypeScript.

Необходимые условия программного обеспечения

Мы напишем приложения для Магазина Windows WinRT / XAML на C # и / или в приложении HTML5 с TypeScript / JavaScript. Поэтому, если вы хотите использовать примеры C # как есть, вам нужно установить:

1 — Windows 8
2 — Visual Studio 2012 Express для приложений Магазина Windows. Вы можете скачать его бесплатно: http://msdn.microsoft.com/en-US/windows/apps/br211386

Если вы решите использовать образцы TypeScript , вам необходимо установить его с: http://www.typescriptlang.org/#Download . Все образцы были обновлены и успешно протестированы с использованием TypeScript 0.9.

Вы найдете плагин для Visual Studio 2012, но есть и другие доступные опции: Sublime Text, Vi, Emacs: TypeScript включен! Со своей стороны, я выучил TypeScript, перенеся версию моего кода на C # на TypeScript. Если вы также заинтересованы в изучении TypeScript, первое хорошее введение — это веб-трансляция: Андерс Хейлсберг: Представляем TypeScript . Пожалуйста, установите также Web Essentials 2012, который имел полную поддержку предварительного просмотра и компиляции TypeScript.

Если вы выбираете JavaScript , вам просто нужна ваша любимая IDE и браузер, совместимый с HTML5. ?

Пожалуйста, создайте проект под названием « SoftEngine », ориентированный на язык, который вы хотите использовать. Если это C # , добавьте « сборку ядра SharpDX », используя NuGet в своем решении:

образ

Если это TypeScript , скачайте babylon.math.ts . Если это JavaScript, скачайте babylon.math.js . Добавьте ссылку на эти файлы в обоих случаях.

Обратный буфер и цикл рендеринга

В 3D-движке мы визуализируем полную сцену в каждом кадре с надеждой сохранить оптимальные 60 кадров в секунду (FPS) для сохранения плавной анимации. Чтобы сделать нашу работу по рендерингу, нам нужно то, что мы называем обратным буфером Это можно рассматривать как двумерный массив, отображающий размер экрана / окна. Каждая ячейка массива сопоставляется с пикселем на экране.

В наших приложениях для Магазина Windows XAML мы будем использовать массив byte [], который будет выступать в качестве нашего динамического резервного буфера . Для каждого кадра, отображаемого в цикле анимации (отметка), этот буфер будет зависеть от WriteableBitmap, действующего как источник элемента управления изображением XAML, который будет называться передним буфером . Для цикла рендеринга мы будем просить механизм рендеринга XAML вызывать нас для каждого сгенерированного кадра. Регистрация осуществляется благодаря этой строке кода:

  CompositionTarget .Rendering + = CompositionTarget_Rendering; 

В HTML5 мы, конечно, будем использовать элемент <canvas /> . Элемент canvas уже имеет массив данных заднего буфера, связанный с ним. Вы можете получить к нему доступ через функции getImageData () и setImageData () . Цикл анимации будет обрабатываться функцией requestAnimationFrame () . Этот гораздо более эффективен, чем эквивалент setTimeout (function () {], 1000/60), поскольку он обрабатывается непосредственно браузером, который будет вызывать наш код только тогда, когда он будет готов к рисованию.

Примечание: в обоих случаях вы можете визуализировать кадры в другом разрешении, отличном от фактической ширины и высоты конечного окна. Например, у вас может быть задний буфер 640 × 480 пикселей, тогда как конечный экран дисплея (передний буфер) будет в 1920 × 1080. В XAML и благодаря CSS в HTML5 вы получите выгоду от « аппаратного масштабирования ». Механизмы рендеринга XAML и браузера будут растягивать данные заднего буфера до окна переднего буфера, даже используя алгоритм сглаживания. В обоих случаях эта задача выполняется графическим процессором. Вот почему мы называем это «аппаратное масштабирование» (аппаратное обеспечение — это графический процессор). Вы можете прочитать больше об этой теме в HTML5 здесь: Раскройте возможности HTML 5 Canvas для игр . Этот подход часто используется в играх, например, для повышения производительности, поскольку у вас меньше пикселей для адресации.

Камера и сетчатые объекты

Давайте начнем кодировать. Во-первых, нам нужно определить некоторые объекты, которые будут включать детали, необходимые для камеры и для меша. Сетка — это классное имя для описания трехмерного объекта.

Наша камера будет иметь 2 свойства: ее положение в трехмерном мире и цель, на которую она смотрит. Оба сделаны из трехмерных координат с именем Vector3. C # будет использовать SharpDX.Vector3, а TypeScript & JavaScript будет использовать BABYLON.Vector3 .

Наша сетка будет иметь набор вершин (несколько вершин или трехмерных точек), которые будут использоваться для построения нашего трехмерного объекта, его положения в трехмерном мире и его состояния вращения. Чтобы идентифицировать это, у этого также будет имя.

Для возобновления нам нужен следующий код:

  // Camera.cs & Mesh.cs 
  используя SharpDX; 
  пространство имен SoftEngine 
     камера общественного класса 
     { 
         public Vector3 Position { get ;  установить ;  } 
         public Vector3 Target { get ;  установить ;  } 
     } 
     общедоступная сетка 
     { 
         публичная строка Name { get ;  установить ;  } 
         public Vector3 [] Vertices { get ;  приватный набор ;  } 
         public Vector3 Position { get ;  установить ;  } 
         public Vector3 Rotation { get ;  установить ;  } 
          общедоступная сетка ( имя строки , int verticesCount) 
         { 
             Vertices = new Vector3 [verticesCount]; 
             Имя = имя; 
         } 
     } 
 
  // <reference path = "babylon.math.ts" /> 
  модуль SoftEngine { 
     класс экспорта Camera { 
         Должность: BABYLON.Vector3; 
         Цель: BABYLON.Vector3; 
          constructor () { 
             this .Position = BABYLON.Vector3.Zero (); 
             this .Target = BABYLON.Vector3.Zero (); 
         } 
     } 
     экспортный класс Mesh { 
         Должность: BABYLON.Vector3; 
         Вращение: BABYLON.Vector3; 
         Вершины: BABYLON.Vector3 []; 
          конструктор ( общедоступное имя: строка , verticesCount: число ) { 
             this .Vertices = new Array (verticesCount); 
             this .Rotation = BABYLON.Vector3.Zero (); 
             this .Position = BABYLON.Vector3.Zero (); 
         } 
     } 
 
  var SoftEngine; 
  function (SoftEngine) { 
     var Camera = ( function () { 
         function Camera () { 
             this .Position = BABYLON.Vector3.Zero (); 
             this .Target = BABYLON.Vector3.Zero (); 
         } 
         возврат камеры; 
     }) (); 
     SoftEngine.Camera = Camera;     
     var Mesh = ( function () { 
         function Mesh (name, verticesCount) { 
             это .name = имя; 
             this .Vertices = new Array (verticesCount); 
             this .Rotation = BABYLON.Vector3.Zero (); 
             this .Position = BABYLON.Vector3.Zero (); 
         } 
         возвратная сетка; 
     }) (); 
     SoftEngine.Mesh = Mesh;     
  ) (SoftEngine || (SoftEngine = {})); 

Например, если вы хотите описать куб, используя наш объект Mesh, вам нужно создать 8 вершин, связанных с 8 точками куба. Вот координаты куба, отображаемого в Blender:

образ

С левосторонним миром. Помните также, что когда вы создаете сетку, система координат начинается с центра сетки. Итак, X = 0, Y = 0, Z = 0 — центр куба.

Это может быть создано с помощью такого кода:

  var mesh = new Mesh ( "Cube" , 8); 
  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); 

Самая важная часть: объект Device

Теперь, когда у нас есть базовые объекты и мы знаем, как создавать трехмерные сетки, нам нужна самая важная часть: объект Device. Это ядро ​​нашего 3D движка .

В его функции рендеринга мы построим матрицу вида и матрицу проекции на основе камеры, которую мы определили ранее.

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

  var transformMatrix = worldMatrix * viewMatrix * projectionMatrix; 

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

Используя эту матрицу преобразования, мы будем проецировать каждую вершину каждой сетки в 2D-мире, чтобы получить координаты X, Y из их координат X, Y, Z. Чтобы наконец нарисовать на экране, мы добавляем небольшую логику клипа для отображения только видимых пикселей с помощью метода / функции PutPixel.

Вот различные версии объекта Device. Я попытался прокомментировать код, чтобы помочь вам понять его в максимально возможной степени.

Примечание. Microsoft Windows рисует с использованием цветового пространства BGRA (синий, зеленый, красный, альфа), тогда как холст HTML5 рисует с использованием цветового пространства RGBA (красный, зеленый, синий, альфа). Вот почему вы заметите небольшие различия в коде между C # и HTML5.

  использование Windows.UI.Xaml.Media.Imaging; 
  using System.Runtime.InteropServices.WindowsRuntime; 
  используя SharpDX; 
  пространство имен SoftEngine 
     Устройство публичного класса 
     { 
         закрытый байт [] backBuffer; 
         private WriteableBitmap bmp; 
          Публичное устройство ( WriteableBitmap BMP) 
         { 
             это .bmp = bmp; 
             // размер заднего буфера равен количеству пикселей для рисования 
             // на экране (ширина * высота) * 4 (значения R, G, B и Alpha).  
             backBuffer = новый байт [bmp.PixelWidth * bmp.PixelHeight * 4]; 
         } 
          // Этот метод вызывается для очистки заднего буфера с определенным цветом 
         public void Clear ( байт r, байт g, байт b, байт a) { 
             for ( var index = 0; index <backBuffer.Length; index + = 4) 
             { 
                 // BGRA используется Windows вместо RGBA в HTML5 
                 backBuffer [index] = b; 
                 backBuffer [index + 1] = g; 
                 backBuffer [index + 2] = r; 
                 backBuffer [index + 3] = a; 
             } 
         } 
          // Как только все будет готово, мы можем очистить задний буфер 
         // в передний буфер.  
         публичный void настоящее () 
         { 
             использование ( var stream = bmp.PixelBuffer.AsStream ()) 
             { 
                 // записываем наш буфер byte [] в наш поток WriteableBitmap 
                 stream.Write (backBuffer, 0, backBuffer.Length); 
             } 
             // запрашиваем перерисовку всего растрового изображения 
             bmp.Invalidate (); 
         } 
          // Вызывается поставить пиксель на экран с определенными координатами X, Y 
         public void PutPixel ( int x, int y, Color4 color) 
         { 
             // Поскольку у нас есть 1-D массив для нашего заднего буфера 
             // нам нужно знать эквивалентную ячейку в 1-D 
             // в 2D координатах на экране 
             var index = (x + y * bmp.PixelWidth) * 4; 
              backBuffer [index] = ( byte ) (color.Blue * 255); 
             backBuffer [index + 1] = ( byte ) (color.Green * 255); 
             backBuffer [index + 2] = ( byte ) (color.Red * 255); 
             backBuffer [index + 3] = ( byte ) (color.Alpha * 255); 
         } 
          // Проект берет несколько трехмерных координат и преобразует их 
         // в 2D координатах с использованием матрицы преобразования 
         общедоступный проект Vector2 ( Vector3ord , Matrix transMat) 
         { 
             // преобразование координат 
             var point = Vector3 .TransformCoordinate (ordinate, TransMat); 
             // Преобразованные координаты будут основаны на системе координат 
             // начиная с центра экрана.  Но рисование на экране обычно начинается 
             // сверху слева.  Затем нам нужно преобразовать их снова, чтобы в верхнем левом углу были значения x: 0, y: 0. 
             var x = point.X * bmp.PixelWidth + bmp.PixelWidth / 2.0f; 
             var y = -point.Y * bmp.PixelHeight + bmp.PixelHeight / 2.0f; 
             return ( новый Vector2 (x, y)); 
         } 
          // DrawPoint вызывает PutPixel, но выполняет операцию отсечения до 
         общественная пустота DrawPoint ( точка Vector2 ) 
         { 
             // Отсечение того, что видно на экране 
             if (point.X> = 0 && point.Y> = 0 && point.X <bmp.PixelWidth && point.Y <bmp.PixelHeight) 
             { 
                 // Рисуем желтую точку 
                 PutPixel (( int ) point.X, ( int ) point.Y, новый Color4 (1.0f, 1.0f, 0.0f, 1.0f)); 
             } 
         } 
          // Основной метод движка, который пересчитывает каждую проекцию вершины 
         // во время каждого кадра 
         public void Render ( Камера , параметры Mesh [], сетки) 
         { 
             // Чтобы понять эту часть, пожалуйста, ознакомьтесь с необходимыми ресурсами 
             var viewMatrix = Matrix .LookAtLH (camera.Position, camera.Target, Vector3 .UnitY); 
             var projectionMatrix = Matrix .PerspectiveFovRH (0,78f,  
                                                            ( float ) bmp.PixelWidth / bmp.PixelHeight,  
                                                            0,01f, 1,0f); 
              foreach ( сетка в сетке)  
             { 
                 // Остерегайтесь применять вращение перед переводом  
                 var worldMatrix = Matrix .RotationYawPitchRoll (mesh.Rotation.Y, 
mesh.Rotation.X, mesh.Rotation.Z) * Matrix .Translation (mesh.Position); var transformMatrix = worldMatrix * viewMatrix * projectionMatrix; foreach ( var vertex in mesh.Vertices) { // Сначала мы проецируем 3D-координаты в 2D-пространство var point = Project (вершина, transformMatrix); // Тогда мы можем рисовать на экране DrawPoint (точка); } } } }
  /// <reference path = "babylon.math.ts" /> 
  модуль SoftEngine { 
       класс экспорта Device { 
         // размер заднего буфера равен количеству пикселей для рисования 
         // на экране (ширина * высота) * 4 (значения R, G, B и Alpha).  
         приватный буфер: ImageData; 
         частная рабочая Canvas: HTMLCanvasElement; 
         приватный рабочийContext: CanvasRenderingContext2D; 
         частная рабочаяШирина: число ; 
         частный рабочийВысота: номер ; 
         // равно backbuffer.data 
         приватные backbufferdata; 
          конструктор (canvas: HTMLCanvasElement) { 
             this .workingCanvas = canvas; 
             this .workingWidth = canvas.width; 
             this .workingHeight = canvas.height; 
             this .workingContext = this .workingCanvas.getContext ( "2d" ); 
         } 
          // Эта функция вызывается для очистки заднего буфера с определенным цветом 
         public clear (): void { 
             // Очистка с черным цветом по умолчанию 
             this .workingContext.clearRect (0, 0, this .workingWidth, this .workingHeight); 
             // после очистки с черными пикселями мы возвращаем связанные данные изображения в  
             // очищаем задний буфер 
             this .backbuffer = this .workingContext.getImageData (0, 0, this .workingWidth, this .workingHeight); 
         } 
          // Как только все будет готово, мы можем очистить задний буфер 
         // в передний буфер.  
         public present (): void { 
             this .workingContext.putImageData ( this .backbuffer, 0, 0); 
         } 
          // Вызывается поставить пиксель на экран с определенными координатами X, Y 
         public putPixel (x: число , y: число , цвет: BABYLON.Color4): void { 
             this .backbufferdata = this .backbuffer.data; 
             // Поскольку у нас есть 1-D массив для нашего заднего буфера 
             // нам нужно знать эквивалентный индекс ячейки в 1-D 
             // на 2D координатах экрана 
             var index: number = ((x >> 0) + (y >> 0) * this .workingWidth) * 4;
   
                    
             // Цветовое пространство RGBA используется холстом HTML5 
             this .backbufferdata [index] = color.r * 255; 
             это .backbufferdata [index + 1] = color.g * 255; 
             this .backbufferdata [index + 2] = color.b * 255; 
             this .backbufferdata [index + 3] = color.a * 255; 
         } 
          // Проект берет несколько трехмерных координат и преобразует их 
         // в 2D координатах с использованием матрицы преобразования 
         общественный проект (координаты: BABYLON.Vector3, transMat: BABYLON.Matrix): BABYLON.Vector2 { 
             // преобразование координат 
             var point = BABYLON.Vector3.TransformCoordinates (координировать, transMat); 
             // Преобразованные координаты будут основаны на системе координат 
             // начиная с центра экрана.  Но рисование на экране обычно начинается 
             // сверху слева.  Затем нам нужно преобразовать их снова, чтобы в верхнем левом углу были значения x: 0, y: 0. 
             var x = point.x * this .workingWidth + this .workingWidth / 2.0 >> 0; 
             var y = -point.y * this .workingHeight + this .workingHeight / 2.0 >> 0; 
             return ( новый BABYLON.Vector2 (x, y)); 
         } 
          // drawPoint вызывает putPixel, но выполняет операцию отсечения до 
         public drawPoint (point: BABYLON.Vector2): void { 
             // Отсечение того, что видно на экране 
             if (point.x> = 0 && point.y> = 0 && point.x < this .workingWidth 
&& point.y < this .workingHeight) { // Рисуем желтую точку this .putPixel (point.x, point.y, новый BABYLON.Color4 (1, 1, 0, 1)); } } // Основной метод движка, который пересчитывает каждую проекцию вершины // во время каждого кадра публичный рендер (камера: камера, сетки: сетка []): void { // Чтобы понять эту часть, пожалуйста, ознакомьтесь с необходимыми ресурсами var viewMatrix = BABYLON.Matrix.LookAtLH (camera.Position, camera.Target, BABYLON.Vector3.Up ()); var projectionMatrix = BABYLON.Matrix.PerspectiveFovLH (0,78,
this .workingWidth / this .workingHeight, 0.01, 1.0); для ( var index = 0; index <meshes.length; index ++) { // текущая сетка для работы var cMesh = meshes [index]; // Остерегайтесь применять вращение перед переводом var worldMatrix = BABYLON.Matrix.RotationYawPitchRoll ( cMesh.Rotation.y, cMesh.Rotation.x, cMesh.Rotation.z) .multiply (BABYLON.Matrix.Translation ( cMesh.Position.x, cMesh.Position.y, cMesh.Position.z)); var transformMatrix = worldMatrix.multiply (viewMatrix) .multiply (projectionMatrix); for ( var indexVertices = 0; indexVertices <cMesh.Vertices.length; indexVertices ++) { // Сначала мы проецируем 3D-координаты в 2D-пространство var projectedPoint = this .project (cMesh.Vertices [indexVertices], transformMatrix); // Тогда мы можем рисовать на экране это .drawPoint (projectedPoint); } } } }
  var SoftEngine; 
  function (SoftEngine) {   
     var Device = ( function () { 
         Функция Device (canvas) { 
             // Примечание: размер заднего буфера равен количеству пикселей для рисования 
             // на экране (ширина * высота) * 4 (значения R, G, B и Alpha).  
             this .workingCanvas = canvas; 
             this .workingWidth = canvas.width; 
             this .workingHeight = canvas.height; 
             this .workingContext = this .workingCanvas.getContext ( "2d" ); 
         } 
          // Эта функция вызывается для очистки заднего буфера с определенным цветом 
         Device.prototype.clear = function () { 
             // Очистка с черным цветом по умолчанию 
             this .workingContext.clearRect (0, 0, this .workingWidth, this .workingHeight); 
             // после очистки с черными пикселями мы возвращаем связанные данные изображения в  
             // очищаем задний буфер 
             this .backbuffer = this .workingContext.getImageData (0, 0, this .workingWidth, this .workingHeight); 
         }; 
          // Как только все будет готово, мы можем очистить задний буфер 
         // в передний буфер.  
         Device.prototype.present = function () { 
             this .workingContext.putImageData ( this .backbuffer, 0, 0); 
         }; 
          // Вызывается поставить пиксель на экран с определенными координатами X, Y 
         Device.prototype.putPixel = function (x, y, color) { 
             this .backbufferdata = this .backbuffer.data; 
             // Поскольку у нас есть 1-D массив для нашего заднего буфера 
             // нам нужно знать эквивалентный индекс ячейки в 1-D 
             // на 2D координатах экрана 
             var index = ((x >> 0) + (y >> 0) * this .workingWidth) * 4;
   
                    
             // Цветовое пространство RGBA используется холстом HTML5 
             this .backbufferdata [index] = color.r * 255; 
             это .backbufferdata [index + 1] = color.g * 255; 
             this .backbufferdata [index + 2] = color.b * 255; 
             this .backbufferdata [index + 3] = color.a * 255; 
         }; 
          // Проект берет несколько трехмерных координат и преобразует их 
         // в 2D координатах с использованием матрицы преобразования 
         Device.prototype.project = function (ordin, TransMat) { 
             var point = BABYLON.Vector3.TransformCoordinates (координировать, transMat); 
             // Преобразованные координаты будут основаны на системе координат 
             // начиная с центра экрана.  Но рисование на экране обычно начинается 
             // сверху слева.  Затем нам нужно преобразовать их снова, чтобы в верхнем левом углу были значения x: 0, y: 0. 
             var x = point.x * this .workingWidth + this .workingWidth / 2.0 >> 0; 
             var y = -point.y * this .workingHeight + this .workingHeight / 2.0 >> 0; 
             return ( новый BABYLON.Vector2 (x, y)); 
         }; 
          // drawPoint вызывает putPixel, но выполняет операцию отсечения до 
         Device.prototype.drawPoint = function (point) { 
             // Отсечение того, что видно на экране 
             if (point.x> = 0 && point.y> = 0 && point.x < this .workingWidth 
&& point.y < this .workingHeight) { // Рисуем желтую точку this .putPixel (point.x, point.y, новый BABYLON.Color4 (1, 1, 0, 1)); } }; // Основной метод движка, который пересчитывает каждую проекцию вершины // во время каждого кадра Device.prototype.render = function (камера, сетки) { // Чтобы понять эту часть, пожалуйста, ознакомьтесь с необходимыми ресурсами var viewMatrix = BABYLON.Matrix.LookAtLH (camera.Position, camera.Target, BABYLON.Vector3.Up ()); var projectionMatrix = BABYLON.Matrix.PerspectiveFovLH (0,78,
this .workingWidth / this .workingHeight, 0.01, 1.0); для ( var index = 0; index <meshes.length; index ++) { // текущая сетка для работы var cMesh = meshes [index]; // Остерегайтесь применять вращение перед переводом var worldMatrix = BABYLON.Matrix.RotationYawPitchRoll ( cMesh.Rotation.y, cMesh.Rotation.x, cMesh.Rotation.z) .multiply (BABYLON.Matrix.Translation ( cMesh.Position.x, cMesh.Position.y, cMesh.Position.z)); var transformMatrix = worldMatrix.multiply (viewMatrix) .multiply (projectionMatrix); for ( var indexVertices = 0; indexVertices <cMesh.Vertices.length; indexVertices ++) { // Сначала мы проецируем 3D-координаты в 2D-пространство var projectedPoint = this .project (cMesh.Vertices [indexVertices], transformMatrix); // Тогда мы можем рисовать на экране это .drawPoint (projectedPoint); } } }; устройство возврата ; }) (); SoftEngine.Device = Device; ) (SoftEngine || (SoftEngine = {}));

Собираем все вместе

Наконец, нам нужно создать сетку (наш куб), создать камеру и нацелить нашу сетку и создать экземпляр нашего объекта Device.

После этого мы запустим цикл анимации / рендеринга. В оптимальных случаях этот цикл будет вызываться каждые 16 мс (60 FPS). Во время каждого тика (вызова обработчика, зарегистрированного в цикле рендеринга), мы будем каждый раз запускать следующую логику:

1 — Очистить экран и все связанные пиксели с черными ( функция Clear () )

2 — Обновите различные значения положения и вращения наших мешей

3 — визуализировать их в задний буфер, выполнив необходимые матричные операции ( функция Render () )

4 — отобразить их на экране, сбросив данные заднего буфера в передний буфер ( функция Present () )

  личное устройство устройства; 
  Mesh mesh = новая сетка ( "Cube" , 8); 
  Camera mera = новая камера (); 
  private void Page_Loaded ( отправитель объекта , RoutedEventArgs e) 
     // Выберите разрешение заднего буфера здесь 
     WriteableBitmap bmp = new WriteableBitmap (640, 480); 
      устройство = новое устройство (bmp); 
      // Наш XAML контроль изображения 
     frontBuffer.Source = bmp; 
      mesh.Vertices [0] = new Vector3 (-1, 1, 1); 
     mesh.Vertices [1] = new Vector3 (1, 1, 1); 
     mesh.Vertices [2] = new Vector3 (-1, -1, 1); 
     mesh.Vertices [3] = новый Vector3 (-1, -1, -1); 
     mesh.Vertices [4] = new Vector3 (-1, 1, -1); 
     mesh.Vertices [5] = new Vector3 (1, 1, -1); 
     mesh.Vertices [6] = новый Vector3 (1, -1, 1); 
     mesh.Vertices [7] = новый Vector3 (1, -1, -1); 
      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); 
      // слегка поворачиваем куб при каждом рендеринге кадра 
     mesh.Rotation = new Vector3 (mesh.Rotation.X + 0.01f, mesh.Rotation.Y + 0.01f, mesh.Rotation.Z); 
      // Выполнение различных матричных операций 
     device.Render (мера, сетка); 
     // Сброс заднего буфера в передний буфер 
     device.Present (); 
 
  /// <reference path = "SoftEngine.ts" /> 
  var canvas: HTMLCanvasElement;  
  вар устройство: SoftEngine.Device; 
  переменная сетка: SoftEngine.Mesh; 
  var meshes: SoftEngine.Mesh [] = []; 
  var mera: SoftEngine.Camera; 
  document.addEventListener ( "DOMContentLoaded" , init, false ); 
  функция init () { 
     canvas = <HTMLCanvasElement> document.getElementById ( "frontBuffer" ); 
     mesh = new SoftEngine.Mesh ( "Cube" , 8); 
     meshes.push (сетка); 
     mera = новый SoftEngine.Camera (); 
     устройство = новый SoftEngine.Device (canvas); 
      mesh.Vertices [0] = новый BABYLON.Vector3 (-1, 1, 1); 
     mesh.Vertices [1] = новый BABYLON.Vector3 (1, 1, 1); 
     mesh.Vertices [2] = новый BABYLON.Vector3 (-1, -1, 1); 
     mesh.Vertices [3] = новый BABYLON.Vector3 (-1, -1, -1); 
     mesh.Vertices [4] = новый BABYLON.Vector3 (-1, 1, -1); 
     mesh.Vertices [5] = новый BABYLON.Vector3 (1, 1, -1); 
     mesh.Vertices [6] = новый BABYLON.Vector3 (1, -1, 1); 
     mesh.Vertices [7] = новый BABYLON.Vector3 (1, -1, -1); 
      mera.Position = new BABYLON.Vector3 (0, 0, 10); 
     mera.Target = новый BABYLON.Vector3 (0, 0, 0); 
      // Вызов цикла рендеринга HTML5 
     requestAnimationFrame (drawingLoop); 
  // Визуализация обработчика цикла 
  function drawingLoop () { 
     device.clear (); 
      // слегка поворачиваем куб при каждом рендеринге кадра 
     mesh.Rotation.x + = 0,01; 
     mesh.Rotation.y + = 0,01; 
      // Выполнение различных матричных операций 
     device.render (мера, сетки); 
     // Сброс заднего буфера в передний буфер 
     device.present (); 
      // рекурсивный вызов цикла рендеринга HTML5 
     requestAnimationFrame (drawingLoop); 
 
  вар холст; 
  вар устройство; 
  сетка вар ; 
  var meshes = []; 
  вар мера; 
  document.addEventListener ( "DOMContentLoaded" , init, false ); 
  функция init () { 
     canvas = document.getElementById ( "frontBuffer" ); 
     mesh = new SoftEngine.Mesh ( "Cube" , 8); 
     meshes.push (сетка); 
     mera = новый SoftEngine.Camera (); 
     устройство = новый SoftEngine.Device (canvas); 
      mesh.Vertices [0] = новый BABYLON.Vector3 (-1, 1, 1); 
     mesh.Vertices [1] = новый BABYLON.Vector3 (1, 1, 1); 
     mesh.Vertices [2] = новый BABYLON.Vector3 (-1, -1, 1); 
     mesh.Vertices [3] = новый BABYLON.Vector3 (-1, -1, -1); 
     mesh.Vertices [4] = новый BABYLON.Vector3 (-1, 1, -1); 
     mesh.Vertices [5] = новый BABYLON.Vector3 (1, 1, -1); 
     mesh.Vertices [6] = новый BABYLON.Vector3 (1, -1, 1); 
     mesh.Vertices [7] = новый BABYLON.Vector3 (1, -1, -1); 
      mera.Position = new BABYLON.Vector3 (0, 0, 10); 
     mera.Target = новый BABYLON.Vector3 (0, 0, 0); 
      // Вызов цикла рендеринга HTML5 
     requestAnimationFrame (drawingLoop); 
  // Визуализация обработчика цикла 
  function drawingLoop () { 
     device.clear (); 
      // слегка поворачиваем куб при каждом рендеринге кадра 
     mesh.Rotation.x + = 0,01; 
     mesh.Rotation.y + = 0,01; 
      // Выполнение различных матричных операций 
     device.render (мера, сетки); 
     // Сброс заднего буфера в передний буфер 
     device.present (); 
      // рекурсивный вызов цикла рендеринга HTML5 
     requestAnimationFrame (drawingLoop); 
 

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

Если нет, загрузите решения, содержащие исходный код:

C # : SoftEngineCSharpPart1.zip

TypeScript : SoftEngineTSPart1.zip

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

Просто просмотрите код и попробуйте найти, что не так с вашим. ?

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

образ

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

Первоначально опубликовано: http://blogs.msdn.com/b/davrous/archive/2013/06/13/tutorial-series-learning-how-to-write-a-3d-soft-engine-from-scratch-in -c-typcript-or-javascript.aspx . Перепечатано здесь с разрешения автора.