譯者前言:
本文譯自[MSDN](http://blogs.msdn.com/b/davrous/archive/2013/06/13/tutorial-series-learning-how-to-write-a-3d-soft-engine-from-scratch-in-c-typescript-or-javascript.aspx),原作者為[David Rousset](https://social.msdn.microsoft.com/profile/david%20rousset/),文章中如果有我的額外說明,我會加上【譯者注:】。
正文開始:

我非常樂意通過一系列教程和大家分享如何建立所謂的”3D軟件渲染引擎“。
”軟件渲染引擎“意味著我們將只使用CPU建立一個3D引擎,完全不使用GPU。(請不要忘記毀滅你的80386?)
我將與你分享C#、TypeScript以及JavaScript三種版本的代碼。在這三者中,你應該可以找到自己喜歡的語言,或者至少比較接近你所喜歡的語言。
這樣做是為了讓你更容易從語法中抽身,把重點放在概念和實現上以及你最喜歡的編程環境中。
代碼在文章末尾可以下載到。
那么,為什么要建立一個3D軟件渲染引擎呢?嗯……這只是因為它確實有助于理解現代化的3D實現以及GPU處理器原理。
事實上,我非常感謝[David Catuhe](http://blogs.msdn.com/b/eternalcoding/)在微軟內部研討會中提供的3D基礎知識。他已經掌握3D知識非常之久,矩陣運算就像是硬編碼一樣刻在了他的腦子里。我在年輕的時候,就一直夢想著能寫這樣的引擎,但是這對我來說太復雜了。但是最后,你會發現這并不復雜。你只是需要一個人幫你理解基本原理以及最簡單的實現方式。
通過本系列教程,你將學習到如何在2D屏幕中繪制一些3D頂點(X,Y,Z)、如何繪制每個點之間的線段、如何填充一些三角形、處理燈光、材質等。
這第一篇教程就先只告訴你如何顯示8個頂點組成一個立方體以及如何在3D虛擬世界中進行移動。
本章教程是以下系列的一部分:
1 – 編寫相機、網格和設備對象的核心邏輯(本文)
[2 – 繪制線段和三角形來獲得線框渲染效果](http://blog.csdn.net/teajs/article/details/49998675)
[3 – 加載通過Blender擴展導出JSON格式的網格](http://blog.csdn.net/teajs/article/details/50001659)
[4 –填充光柵化的三角形并使用深度緩沖](http://blog.csdn.net/teajs/article/details/50010073)
[4b – 額外章節:使用技巧和并行處理來提高性能](http://blog.csdn.net/teajs/article/details/50054509)
[5 – 使用平面著色和高氏著色處理光 ?](http://blogs.msdn.com/b/davrous/archive/2013/07/03/tutorial-part-5-learning-how-to-write-a-3d-software-engine-in-c-ts-or-js-flat-amp-gouraud-shading.aspx)
[6 – 應用紋理、背面剔除以及一些WebGL相關](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-texture-mapping-back-face-culling-amp-webgl.aspx)
如果按照完整的系列教程學習,你就會**知道如何建立自己的3D軟件渲染引擎**!引擎將會描繪一些線框,然后光柵化,再進行高氏著色,最后應用紋理。
[](http://david.blob.core.windows.net/html5/SoftEngineProgression/wireframe/index.html)[](http://david.blob.core.windows.net/html5/SoftEngineProgression/rasterization/index.html)[](http://david.blob.core.windows.net/html5/SoftEngineProgression/gouraudshading/index.html)[](http://david.blob.core.windows.net/html5/SoftEngineProgression/textured/index.html)
點擊圖片查看階段演示,在本系列教程中我們將討論如何從線框到最終的紋理。
點擊[運行代碼](http://david.blob.core.windows.net/softengine3d/part1/index.html)后,你將看到學習本章節將會實現8個點的效果。
**免責聲明**:
出于教學目的,所以我要建立一個這樣的3D軟件渲染引擎,而不是使用GPU來渲染。
當然,如果你需要建立一個游戲流體3D動畫,你需要的是DirectX或OpenGL/WebGL之類的。
但是,一但你明白如何建立一個3D軟件渲染引擎,那么更”復雜“的引擎將更容易理解。
再進一步,你絕對應該看看由David Catuhe建立的[BabylonJS WebGL引擎](http://www.babylonjs.com/)。
更多詳情及教程在這里:[Babylon.js,使用Html5和WebGL的一個完整JavaScript框架來構建游戲。](http://www.microsoftvirtualacademy.com/training-courses/introduction-to-webgl-3d-with-html5-and-babylon-js)
**查看MVA視頻培訓版本**:我們已經與David Catuhe做了一個免費的8個單元的課程,讓你學習到基本3D知識、WebGL和[Babylon.js](http://www.babylonjs.com/)。
第一單元是包含本系列教程的一個40分鐘的視頻版本:[介紹Html5 WebGL 3D和Babylon.js](http://www.microsoftvirtualacademy.com/training-courses/introduction-to-webgl-3d-with-html5-and-babylon-js)
[](https://mva.microsoft.com/en-US/training-courses/introduction-to-webgl-3d-with-html5-and-babylon-js-8421)
**閱讀前提條件**
我用了很長一段時間一直在思考如何些這些教程。現在我終于決定不再解釋每個必須了解的原理。
在網絡上有很多不錯的資源,比我能更好的解釋這些關鍵原理。
但我也花了一段時間為大家挑選了一些頁面,請根據最適合自己的進行閱讀:【譯者注:某些鏈接已無效,在此已刪除】
[- 世界矩陣、視圖矩陣和投影矩陣揭秘](http://web.archive.org/web/20131222170415/http:/robertokoci.com/world-view-projection-matrix-unveiled/)
[- 教程3:矩陣](http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/) 將為您簡要介紹一下模型矩陣、視圖矩陣與投影矩陣
[- 變換 (Direct3D 9)](https://msdn.microsoft.com/en-us/library/windows/desktop/bb206269(v=vs.85).aspx)
[- 3D簡要介紹](http://inear.se/talk/a_brief_introduction_to_3d.pptx):一個優秀的PPT幻燈片!請至少讀到27頁,在此之后的內容也談到OpenGL或DirectX鏈接到GPU的技術。
[- OpenGL變換](http://www.songho.ca/opengl/gl_transform.html)

或許你已經知道了一些三角形的概念,這與API無關(OpenGL或DirectX),我們以后將看到。
通過閱讀這些文章,你真正需要了解的是,有這樣一連串的變換:
- 我們先圍繞一個三維物體本身
- 對同一個對象移入虛擬的3D世界中通過矩陣操作進行平移、縮放或旋轉
- 在3D世界中攝像機朝向這個三維物體
- 這個流程之后最終的結果將會投射在一個二維空間,也就是你的屏幕上


這一切都是通過矩陣神奇的運算累計變換完成的。在教程示例運行之前,你真的應該稍微了解這些概念。就算你不讀這些文章就明白了這一切也應該去掃一眼,因為你日后在寫3D軟件渲染引擎時很可能會回去看。不過這是完全正常的,不用擔心 !;) ?因為最好的3D學習方式是通過試錯。
我們不會花時間說矩陣是如何工作的,好消息是,你也不需要真正了解矩陣。我們簡單的把它看成一個黑盒子,然后做正確的操作就行了。我不是矩陣的高手,但是我可以設法自己編寫3D軟件渲染引擎。所以,你這樣做也可以取得成功。
然后,我們將使用為我們工作的庫:
對于C#開發人員來說,我們可以用SharpDX,是一個DirectX的托管包裝庫。
對于JavaScript開發人員來說,我們可以使用由David Catuhe編寫的Babylon.math.js庫。
同時,我用TypeScript重寫了Babylon.math.js庫。
所需要的軟件
我們可以編寫C#語言開發的WinRT/XAML Windows Store Apps應用程序,或使用TypeScript/JavaScript開發的Html5應用程序。
那么,如果你想要使用C#進行開發,你需要安裝:
1 - Windows 8及以上版本的操作系統
2 - Visual Studio Express for Windows Store Apps([點此下載](http://msdn.microsoft.com/en-US/windows/apps/br211386))或以上版本的Visual Studio IDE。
如果你選擇使用TypeScript編寫,你需要從這里[安裝](http://www.typescriptlang.org/#Download)此語言。
你會發現這個插件是Visual Studio 2012版本的,但還有其他的選擇:[Sublime Text, Vi, Emacs:TypeScript支持](http://msopentech.com/blog/2012/10/01/sublime-text-vi-emacs-typescript-enabled)!
【譯者注:此處省略100個英文字母,給TypeScript打廣告太明顯,偏離了本章主題】
如果你選擇JavaScript,你只需要安裝你喜歡的IDE和Html5兼容的瀏覽器。:)
請使用你喜歡的語言創建一個名為“SoftEngine”的項目。如果選擇的語言是C#,請使用NuGet添加“SharpDX core assembly”到你的解決方案中:

如果是TypeScript,請下載[Babylon.math.ts](http://david.blob.core.windows.net/softengine3d/babylon.math.ts)。如果是JavaScrip,請下載[Babylon.math.js](http://david.blob.core.windows.net/softengine3d/babylon.math.js)。并進行引用。
**后臺緩沖區 & 渲染循環**
一個3D引擎,我們繪制完整的場景以保持每秒60幀(FPS)為最佳。這樣可以保證動畫的流暢。
為了進行渲染工作,我們需要后臺緩沖區。它可以被看作是一個映射屏幕大小區域的二維數組。數組中的每個元素都被映射為屏幕上的某一個像素。
在XAMLWindows Store Apps中,我們將使用一個byte[]數組,它將成為我們的動態后臺緩沖區。
在我們的動畫循環中(Tick),每一幀要進行渲染時,后臺緩沖區都將影響到作為XAML圖像源頭的前臺緩沖區的WriteableBitmap。
在渲染循環中,XAML渲染引擎調用我們時,幀就會被產生。注冊事件代碼為:
~~~
CompositionTarget.Rendering += CompositionTarget_Rendering;
~~~
在Html5中我們將使用 <canvas /> 元素。canvas元素已經具有一個后臺緩沖區數組的關聯。你可以通過getImageData()和setImageData()函數來訪問它。
動畫循環將有 [requestAnimationFrame()](http://msdn.microsoft.com/library/ie/hh920765(v=vs.85).aspx) 函數來處理。它是一個類似 setTimeout(function(){}, 1000 / 60); 的定時器實現。不同的是,只有當瀏覽器主動調用時才會執行,所以并不需要指定間隔時間。
**注意**:在”硬件縮放“的情況下,后臺緩沖區和前臺緩沖區的大小可以不同。使用”硬件縮放“可以得到更好的性能和更差的效果【譯者注:我是故意把這里翻譯成這樣的,很喜感不是么……哈哈。】。更多關于這個話題的內容請看[這里](http://blogs.msdn.com/b/eternalcoding/archive/2012/03/22/unleash-the-power-of-html-5-canvas-for-gaming-part-1.aspx?Redirected=true)。
**攝像機 & 網格 對象**
讓我們開始編碼吧!首先,我們需要定義一些有關于攝像機和網格的細節。網格這個名稱用來代指三維物體。
我們的攝像機有2個屬性:它在3D世界的位置以及它所看向的方位。兩者都是Vector3類型。在C#中用 SharpDX.Vector3,在TypeScript & JavaScript中使用BABYLON.Vector3。
我們的網格有頂點數組 (一些三維點),這將用來構建我們的三維物體,它在3D世界中的位置和它旋轉的狀態。
繼續,我們需要下面的代碼:
【譯者注:C#代碼】
~~~
// Camera.cs & Mesh.cs
using SharpDX;
namespace SoftEngine
{
public class Camera
{
public Vector3 Position { get; set; }
public Vector3 Target { get; set; }
}
public class Mesh
{
public string Name { get; set; }
public Vector3[] Vertices { get; private set; }
public Vector3 Position { get; set; }
public Vector3 Rotation { get; set; }
public Mesh(string name, int verticesCount)
{
Vertices = new Vector3[verticesCount];
Name = name;
}
}
}
~~~
【譯者注:TypeScript代碼】
~~~
//<reference path="babylon.math.ts"/>
module SoftEngine {
export class Camera {
Position: BABYLON.Vector3;
Target: BABYLON.Vector3;
constructor() {
this.Position = BABYLON.Vector3.Zero();
this.Target = BABYLON.Vector3.Zero();
}
}
export class Mesh {
Position: BABYLON.Vector3;
Rotation: BABYLON.Vector3;
Vertices: BABYLON.Vector3[];
constructor(public name: string, verticesCount: number) {
this.Vertices = new Array(verticesCount);
this.Rotation = BABYLON.Vector3.Zero();
this.Position = BABYLON.Vector3.Zero();
}
}
}
~~~
【譯者注:JavaScript代碼】
~~~
var SoftEngine;
(function (SoftEngine) {
var Camera = (function () {
function Camera() {
this.Position = BABYLON.Vector3.Zero();
this.Target = BABYLON.Vector3.Zero();
}
return Camera;
})();
SoftEngine.Camera = Camera;
var Mesh = (function () {
function Mesh(name, verticesCount) {
this.name = name;
this.Vertices = new Array(verticesCount);
this.Rotation = BABYLON.Vector3.Zero();
this.Position = BABYLON.Vector3.Zero();
}
return Mesh;
})();
SoftEngine.Mesh = Mesh;
})(SoftEngine || (SoftEngine = {}));
~~~
舉例來說,如果你想使用我們的網格對象來描述一個立方體,你需要創建8個頂點(vetices)來關聯到8個點(points)。下面是在Blender中顯示的立方體坐標。

用的是左手坐標系。請別忘了,當你創建一個網格,坐標系開始點再網格的中心。因此,X=0, Y=0, Z=0是立方體的中心點。
這個立方體可以通過這樣的代碼來創建:
~~~
var mesh = new Mesh("Cube", 8);
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] = new Vector3(-1, -1, -1);
mesh.Vertices[4] = new Vector3(-1, 1, -1);
mesh.Vertices[5] = new Vector3(1, 1, -1);
mesh.Vertices[6] = new Vector3(1, -1, 1);
mesh.Vertices[7] = new Vector3(1, -1, -1);
~~~
**最重要的部分:設備對象**
現在,我們有了基本對象,我們知道如何構建一個三維網格。但是我們還缺少一個最重要的部分:設備對象。這是我們的3D引擎核心部分。
在引擎渲染的函數內,我們將建立投影矩陣,并根據我們預先定義過攝像機獲得視圖矩陣。然后我們遍歷每個網格提供基于目前的旋轉和平移值來構建世界矩陣。
最后,得到這三個矩陣后,我們就可以這樣得到最終的變換矩陣:
~~~
var transformMatrix = worldMatrix * viewMatrix * projectionMatrix;
~~~
你絕對需要通過閱讀前面的“閱讀前提條件”來理解這個概念。否則,你可能會簡單的Copy/Paste代碼,而無須了解有關神奇的實現。這對于之后的學習并沒有太大的影響,但是理解它你能更好的進行編碼。
使用該變換矩陣,我們將項目中每個網格的每個頂點從X, Y, Z坐標轉換到2D世界中的X, Y坐標,最終在屏幕上繪制。
我們增加一小段邏輯,只通過 PutPixel(方法/函數)進行繪制顯示。
這里有各種版本的設備對象。我增加了些注釋,以幫助您更多的理解它。【譯者注:還不是要一個一個翻譯!】
**注**:微軟Windows使用BGRA顏色空間(藍色,綠色,紅色,阿爾法),而Html5的畫布使用的是[RGBA](http://en.wikipedia.org/wiki/RGBA_color_space)顏色空間(紅,綠,藍,阿爾法)。
這就是你就發現為什么C#和Html5代碼有一些小差別的原因。
【譯者注:C#代碼】
~~~
using Windows.UI.Xaml.Media.Imaging;
using System.Runtime.InteropServices.WindowsRuntime;
using SharpDX;
namespace SoftEngine
{
public class Device
{
private byte[] backBuffer;
private WriteableBitmap bmp;
public Device(WriteableBitmap bmp)
{
this.bmp = bmp;
// 后臺緩沖區大小值是要繪制的像素
// 屏幕(width*height) * 4 (R,G,B & Alpha值)
backBuffer = new byte[bmp.PixelWidth * bmp.PixelHeight * 4];
}
// 清除后臺緩沖區為指定顏色
public void Clear(byte r, byte g, byte b, byte a)
{
for (var index = 0; index < backBuffer.Length; index += 4)
{
// Windows使用BGRA,而不是Html5中使用的RGBA
backBuffer[index] = b;
backBuffer[index + 1] = g;
backBuffer[index + 2] = r;
backBuffer[index + 3] = a;
}
}
// 當一切準備就緒時,我們就可以
// 刷新后臺緩沖區到前臺緩沖區
public void Present()
{
using (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)
{
// 我們的后臺緩沖區是一維數組
// 這里我們簡單計算,將X和Y對應到此一維數組中
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);
}
// 將三維坐標和變換矩陣轉換成二維坐標
public Vector2 Project(Vector3 coord, Matrix transMat)
{
// 進行坐標變換
var point = Vector3.TransformCoordinate(coord, transMat);
// 變換后的坐標起始點是坐標系的中心點
// 但是,在屏幕上,我們以左上角為起始點
// 我們需要重新計算使他們的起始點變成左上角
var x = point.X * bmp.PixelWidth + bmp.PixelWidth / 2.0f;
var y = -point.Y * bmp.PixelHeight + bmp.PixelHeight / 2.0f;
return (new Vector2(x, y));
}
// 如果二維坐標在可視范圍內則繪制
public void DrawPoint(Vector2 point)
{
// 判斷是否在屏幕內
if (point.X >= 0 && point.Y >= 0 && point.X < bmp.PixelWidth && point.Y < bmp.PixelHeight)
{
// 繪制一個黃色點
PutPixel((int)point.X, (int)point.Y, new Color4(1.0f, 1.0f, 0.0f, 1.0f));
}
}
// 主循環體,每一幀,引擎都要計算頂點投射
public void Render(Camera camera, params Mesh[] meshes)
{
// 要理解這個部分,請閱讀“閱讀前提條件”
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 (Mesh mesh in meshes)
{
// 請注意,在平移前要先旋轉
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)
{
// 首先,我們將三維空間轉換為二維空間
var point = Project(vertex, transformMatrix);
// 然后我們就可以在屏幕畫出點
DrawPoint(point);
}
}
}
}
}
~~~
【譯者注: TypeScript代碼】
~~~
///<reference path="babylon.math.ts"/>
module SoftEngine {
export class Device {
// 后臺緩沖區大小值是要繪制的像素
// 屏幕(width*height) * 4 (R,G,B & Alpha值)
private backbuffer: ImageData;
private workingCanvas: HTMLCanvasElement;
private workingContext: CanvasRenderingContext2D;
private workingWidth: number;
private workingHeight: number;
// 等于backbuffer.data
private backbufferdata;
constructor(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: number, y: number, color: BABYLON.Color4): void {
this.backbufferdata = this.backbuffer.data;
// 我們的后臺緩沖區是一維數組
// 這里我們簡單計算,將X和Y對應到此一維數組中
var index: number = ((x >> 0) + (y >> 0) * this.workingWidth) * 4;
// 在Html5 canvas中使用RGBA顏色空間
this.backbufferdata[index] = color.r * 255;
this.backbufferdata[index + 1] = color.g * 255;
this.backbufferdata[index + 2] = color.b * 255;
this.backbufferdata[index + 3] = color.a * 255;
}
// 將三維坐標和變換矩陣轉換成二維坐標
public project(coord: BABYLON.Vector3, transMat: BABYLON.Matrix): BABYLON.Vector2 {
// 進行坐標變換
var point = BABYLON.Vector3.TransformCoordinates(coord, transMat);
// 變換后的坐標起始點是坐標系的中心點
// 但是,在屏幕上,我們以左上角為起始點
// 我們需要重新計算使他們的起始點變成左上角
var x = point.x * this.workingWidth + this.workingWidth / 2.0 >> 0;
var y = -point.y * this.workingHeight + this.workingHeight / 2.0 >> 0;
return (new BABYLON.Vector2(x, y));
}
// 如果二維坐標在可視范圍內則繪制
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, new BABYLON.Color4(1, 1, 0, 1));
}
}
// 主循環體,每一幀,引擎都要計算頂點投射
public render(camera: Camera, meshes: Mesh[]): 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);
for (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++) {
// 首先,我們將三維空間轉換為二維空間
var projectedPoint = this.project(cMesh.Vertices[indexVertices], transformMatrix);
// 然后我們就可以在屏幕畫出點
this.drawPoint(projectedPoint);
}
}
}
}
}
~~~
【譯者注:JavaScript代碼】
~~~
var SoftEngine;
(function (SoftEngine) {
var Device = (function () {
function Device(canvas) {
// 后臺緩沖區大小值是要繪制的像素
// 屏幕(width*height) * 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;
// 我們的后臺緩沖區是一維數組
// 這里我們簡單計算,將X和Y對應到此一維數組中
var index = ((x >> 0) + (y >> 0) * this.workingWidth) * 4;
// 在Html5 canvas中使用RGBA顏色空間
this.backbufferdata[index] = color.r * 255;
this.backbufferdata[index + 1] = color.g * 255;
this.backbufferdata[index + 2] = color.b * 255;
this.backbufferdata[index + 3] = color.a * 255;
};
// 將三維坐標和變換矩陣轉換成二維坐標
Device.prototype.project = function (coord, transMat) {
// 進行坐標變換
var point = BABYLON.Vector3.TransformCoordinates(coord, transMat);
// 變換后的坐標起始點是坐標系的中心點
// 但是,在屏幕上,我們以左上角為起始點
// 我們需要重新計算使他們的起始點變成左上角
var x = point.x * this.workingWidth + this.workingWidth / 2.0 >> 0;
var y = -point.y * this.workingHeight + this.workingHeight / 2.0 >> 0;
return (new BABYLON.Vector2(x, y));
};
// 如果二維坐標在可視范圍內則繪制
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, new BABYLON.Color4(1, 1, 0, 1));
}
};
// 主循環體,每一幀,引擎都要計算頂點投射
Device.prototype.render = function (camera, meshes) {
// 要理解這個部分,請閱讀“閱讀前提條件”
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);
for (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++) {
// 首先,我們將三維空間轉換為二維空間
var projectedPoint = this.project(cMesh.Vertices[indexVertices], transformMatrix);
// 然后我們就可以在屏幕畫出點
this.drawPoint(projectedPoint);
}
}
};
return Device;
})();
SoftEngine.Device = Device;
})(SoftEngine || (SoftEngine = {}));
~~~
**結合到一起**
最后我們需要建立一個網格(我們的立方體),創建一個攝像機,并面向我們的網格,然后實例化設備對象。
一旦這樣做,我們將運行動畫/渲染循環。在理想的情況下,這個循環將每隔16毫秒(60FPS)執行一次。在每個循環周期,做了這樣幾件事:
1 - 清空屏幕并且將所有像素變黑(*Clear()* function)。
2 - 對網格更新位置和旋轉值。
3 - 計算矩陣,渲染到后臺緩沖區( * Render()* function)。
4 - 將后臺緩沖區數據刷新到前臺緩沖區以顯示( * Present()* function)。
【譯者注:C#代碼】
~~~
private Device device;
Mesh mesh = new Mesh("Cube", 8);
Camera mera = new Camera();
private void Page_Loaded(object sender, RoutedEventArgs e)
{
// 在這里設置后臺緩沖區的分辨率
WriteableBitmap bmp = new WriteableBitmap(640, 480);
device = new Device(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] = new Vector3(-1, -1, -1);
mesh.Vertices[4] = new Vector3(-1, 1, -1);
mesh.Vertices[5] = new Vector3(1, 1, -1);
mesh.Vertices[6] = new Vector3(1, -1, 1);
mesh.Vertices[7] = new Vector3(1, -1, -1);
mera.Position = new Vector3(0, 0, 10.0f);
mera.Target = Vector3.Zero;
// 注冊XAML渲染循環
CompositionTarget.Rendering += CompositionTarget_Rendering;
}
// 渲染循環處理
void CompositionTarget_Rendering(object sender, object 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(mera, mesh);
// 刷新后臺緩沖區到前臺緩沖區
device.Present();
}
~~~
【譯者注:TypeScript代碼】
~~~
///<reference path="SoftEngine.ts"/>
var canvas: HTMLCanvasElement;
var device: SoftEngine.Device;
var mesh: SoftEngine.Mesh;
var meshes: SoftEngine.Mesh[] = [];
var mera: SoftEngine.Camera;
document.addEventListener("DOMContentLoaded", init, false);
function init()
{
canvas = < HTMLCanvasElement > document.getElementById("frontBuffer");
mesh = new SoftEngine.Mesh("Cube", 8);
meshes.push(mesh);
mera = new SoftEngine.Camera();
device = new SoftEngine.Device(canvas);
mesh.Vertices[0] = new BABYLON.Vector3(-1, 1, 1);
mesh.Vertices[1] = new BABYLON.Vector3(1, 1, 1);
mesh.Vertices[2] = new BABYLON.Vector3(-1, -1, 1);
mesh.Vertices[3] = new BABYLON.Vector3(-1, -1, -1);
mesh.Vertices[4] = new BABYLON.Vector3(-1, 1, -1);
mesh.Vertices[5] = new BABYLON.Vector3(1, 1, -1);
mesh.Vertices[6] = new BABYLON.Vector3(1, -1, 1);
mesh.Vertices[7] = new BABYLON.Vector3(1, -1, -1);
mera.Position = new BABYLON.Vector3(0, 0, 10);
mera.Target = new BABYLON.Vector3(0, 0, 0);
// 調用Html5渲染循環
requestAnimationFrame(drawingLoop);
}
// 渲染循環處理
function drawingLoop()
{
device.clear();
// 每幀都稍微轉動一下立方體
mesh.Rotation.x += 0.01;
mesh.Rotation.y += 0.01;
// 做各種矩陣運算
device.render(mera, meshes);
// 刷新后臺緩沖區到前臺緩沖區
device.present();
// 遞歸調用Html5渲染循環
requestAnimationFrame(drawingLoop);
}
~~~
【譯者注:JavaScript代碼】
~~~
var canvas;
var device;
var mesh;
var meshes = [];
var mera;
document.addEventListener("DOMContentLoaded", init, false);
function init() {
canvas = document.getElementById("frontBuffer");
mesh = new SoftEngine.Mesh("Cube", 8);
meshes.push(mesh);
mera = new SoftEngine.Camera();
device = new SoftEngine.Device(canvas);
mesh.Vertices[0] = new BABYLON.Vector3(-1, 1, 1);
mesh.Vertices[1] = new BABYLON.Vector3(1, 1, 1);
mesh.Vertices[2] = new BABYLON.Vector3(-1, -1, 1);
mesh.Vertices[3] = new BABYLON.Vector3(-1, -1, -1);
mesh.Vertices[4] = new BABYLON.Vector3(-1, 1, -1);
mesh.Vertices[5] = new BABYLON.Vector3(1, 1, -1);
mesh.Vertices[6] = new BABYLON.Vector3(1, -1, 1);
mesh.Vertices[7] = new BABYLON.Vector3(1, -1, -1);
mera.Position = new BABYLON.Vector3(0, 0, 10);
mera.Target = new BABYLON.Vector3(0, 0, 0);
// 調用Html5渲染循環
requestAnimationFrame(drawingLoop);
}
// 渲染循環處理
function drawingLoop() {
device.clear();
// 每幀都稍微轉動一下立方體
mesh.Rotation.x += 0.01;
mesh.Rotation.y += 0.01;
// 做各種矩陣運算
device.render(mera, meshes);
// 刷新后臺緩沖區到前臺緩沖區
device.present();
// 遞歸調用Html5渲染循環
requestAnimationFrame(drawingLoop);
}
~~~
如果你已經正確的遵循這第一個教程的話,你應該已經得到這樣的效果:
[點我運行](http://david.blob.core.windows.net/softengine3d/part1/index.html)
如果沒有,下載源代碼:
C#:[SoftEngineCSharpPart1.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineCSharpPart1.zip)
TypeScript:[SoftEngineTSPart1.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineTSPart1.zip)
JavaScript:[SoftEngineJSPart1.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineJSPart1.zip) 或只需右鍵點擊 -> 查看框架的源代碼
簡單的檢查代碼并試圖找到是什么地方除了差錯。 :)
下一章節,我們將學習面和三角形的概念來繪制每個頂點之間的線段。
