譯者前言:
本文譯自[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/),文章中如果有我的額外說明,我會加上【譯者注:】。
正文開始:
在之前的教程中,我們學習了如何通過C#、TypeScript、JavaScript使用光柵化和深度緩沖來填充我們的三角形。由于我們的3D軟件渲染引擎使用的是CPU運算,因此它將耗費大量的CPU處理時間。不過倒是有一個好消息,那就是CPU大多是多核心的。那么,我們可以想象一下使用并行處理來提高引擎性能。不過我們只能在C#中這么做,至于為什么Html5不可以,我將會稍后做出解釋。我們在此篇章中可以學到一些簡單的技巧,以此達到優化渲染性能的目的。而且實際上,我們也得到了從5 FPS提升到50 FPS的結果,這可是10倍的性能提升!
本章教程是以下系列的一部分:
[1 – 編寫相機、網格和設備對象的核心邏輯](http://blog.csdn.net/teajs/article/details/49989681)
[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 – 額外章節:使用技巧和并行處理來提高性能(本文)
[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)
**計算FPS**
首先第一步我們需要知道FPS,這是性能體現的一個重要指標。我們不論在C#、TypeScript或JavaScript中都可以實現FPS的統計。
首先我們需要知道兩幀之間的執行間隔時間。
我們要進行循環渲染,在Html5中使用requestAnimationFrame,在XAML中使用Composition.Rendering,它們會自動管理循環時間。
一般而言,最佳間隔時間在 1000(每秒毫秒數) / 60(顯示器最大刷新率) 毫秒內,也就是 ≈?16.666667 毫秒。
這里有一個David和我一起做的[基準測試專題](http://blogs.msdn.com/b/eternalcoding/archive/2013/05/21/benchmarking-a-html5-game-html5-potatoes-gaming-bench.aspx)。
綜上所訴,在C#中,添加一個新的XAML TextBlock元素,將其命名為"fps",用于顯示當前FPS值:
~~~
DateTime previousDate;
void CompositionTarget_Rendering(object sender, object e)
{
// Fps
var now = DateTime.Now;
var currentFps = 1000.0 / (now - previousDate).TotalMilliseconds;
previousDate = now;
fps.Text = string.Format("{0:0.00} fps", currentFps);
// Rendering loop
device.Clear(0, 0, 0, 255);
foreach (var mesh in meshes)
{
mesh.Rotation = new Vector3(mesh.Rotation.X, mesh.Rotation.Y + 0.01f, mesh.Rotation.Z);
device.Render(mera, mesh);
}
device.Present();
}
~~~
我在我的聯想 Carbon X1 Touch(分辨率為1600x900)電腦上運行前面的C#版示例,每秒5 FPS。我的聯想電腦CPU為Intel Core I7-3667U(4個邏輯處理器)以及一個HD4000核心顯卡。
**優化和并行處理策略**
WinRT應用程序使用.Net Framework4.5版本,其中默認(TPL)包含了任務并行庫(Task Parallel Library)。如果你留心寫算法的方式,并且你的算法可以并行處理,那么使之并行化處理將變得非常容易。如果你不了解這個概念,可以先看看[并行處理的.Net Framework4: 入門](http://blogs.msdn.com/b/csharpfaq/archive/2010/06/01/parallel-programming-in-net-framework-4-getting-started.aspx)
**避免接觸UI控件**
多線程/多任務的第一條規則是沒有一點關于UI控件代碼存在。只有UI線程可以接觸/操控圖形控件。但在我們的例子中,有一段代碼訪問到bmp.PixelWidth和bmp.PixelHeight。bmp是WriteableBitmap類型,它被認為是一個UI元素,而不是線程安全的。這就是為什么,我們需要先修改這些代碼才能使他們“并行”。在之前的教程中,我們就已經開始這么做了,那就是將bmp中的PixelWidth和PixelHeight的引用全部更改為renderWidth和renderHeight就可以了。
順便說一句,這個規則不僅是并行處理所必須的,也是一般的性能優化。因此,通過簡單的緩存機制把WriteableBitmap轉移到我們的變量中便可以達到從5 FPS提升到45 FPS!
在Html5中這個規則也是非常重要的。你應該避免直接訪問DOM元素的屬性,因為DOM操作非常慢。所以,它并不以16毫秒每幀的運行速度在運行。我們最好始終緩存接下來要使用到的值。不過我們已經在之前的教程中這么做了。
**自給自足**
第二個原則是,代碼將在多個可用核心中自給自足。你的代碼不能等待很長時間,否則將抵消并行處理的優勢。不過幸運的是,在我們的例子中,一直遵循著這個規則。
你可能已經看到,我們有幾個地方用Parallel.For替換了經典的循環以此來并行處理。
第一個例子是DrawTriangle方法中。我們將用并行處理用幾條線畫三角形。在此,你可以很容易的將2個正常循環替換為Parallel.For循環:
~~~
if (dP1P2 > dP1P3)
{
Parallel.For((int)p1.Y, (int)p3.Y + 1, y =>
{
if (y < p2.Y)
{
ProcessScanLine(y, p1, p3, p1, p2, color);
}
else
{
ProcessScanLine(y, p1, p3, p2, p3, color);
}
});
}
else
{
Parallel.For((int)p1.Y, (int)p3.Y + 1, y =>
{
if (y < p2.Y)
{
ProcessScanLine(y, p1, p2, p1, p3, color);
}
else
{
ProcessScanLine(y, p2, p3, p1, p3, color);
}
});
}
~~~
但是,在我的情況下,輸出的結果有一點點出乎意料。我的性能被降低了,竟然從45 FPS跌到了40 FPS!那么,是什么原因導致了這個問題呢?
那么,出現這樣的情況,顯然不是使用并行處理就夠了。我們需要花更多的時間用于上下文切換,也就是從一個核心移動到另一個核心而不是真正并行處理。你可以使用Visual Studio 2012的嵌入式分析工具:[Visual Studio 2012并行可視化](http://msdn.microsoft.com/en-us/library/dd537632.aspx)來查看。
下面是第一種并行方式的核心利用圖:

各種顏色都與工作線程有關,這真的沒有效率可言。下面再看看**非并行版本**的核心利用圖:

我們只有一個線程在工作(綠色的),所顯示的是由操作系統派出的一些核心。即使我們不明確使用并行,CPU也會自動使用多核心處理。顯然我們的第一種并行方案上下文切換太頻繁了。
**互斥鎖和適當循環的并行處理**
嗯,我猜你和我得出了同樣的結論。并行化的drawTriangle循環方案似乎并沒有成為一個很好的選擇。我們需要另外的東西來讓線程切換更加高效。我們要并行處理多個三角形,而不是繪制一個三角形。總之,每一個核心都要繪制一個完整的三角形。
這種方法的問題將會在PutPixel方法中出現。現在我們要并行處理幾個面,我們可能會陷入2個內核/線程試圖并行訪問同一個像素的情況。這是,我們只需要保護要訪問的像素就可以了。事實上,如果我們用更多時間去保護正在工作中的數據,并行化將會非常有用。
解決的辦法是使用一個鎖:
~~~
private object[] lockBuffer;
public Device(WriteableBitmap bmp)
{
this.bmp = bmp;
renderWidth = bmp.PixelWidth;
renderHeight = bmp.PixelHeight;
// 后臺緩沖區大小值是要繪制的像素
// 屏幕(width*height) * 4 (R,G,B & Alpha值)
backBuffer = new byte[renderWidth * renderHeight * 4];
depthBuffer = new float[renderWidth * renderHeight];
lockBuffer = new object[renderWidth * renderHeight];
for (var i = 0; i < lockBuffer.Length; i++)
{
lockBuffer[i] = new object();
}
}
// 調用此方法把一個像素繪制到指定的X, Y坐標上
public void PutPixel(int x, int y, float z, Color4 color)
{
// 我們的后臺緩沖區是一維數組
// 這里我們簡單計算,將X和Y對應到此一維數組中
var index = (x + y * renderWidth);
var index4 = index * 4;
// 使用鎖來保護緩沖區不被并行處理擾亂
lock (lockBuffer[index])
{
if (depthBuffer[index] < z)
{
return; // 深度測試不通過
}
depthBuffer[index] = z;
backBuffer[index4] = (byte)(color.Blue * 255);
backBuffer[index4 + 1] = (byte)(color.Green * 255);
backBuffer[index4 + 2] = (byte)(color.Red * 255);
backBuffer[index4 + 3] = (byte)(color.Alpha * 255);
}
}
~~~
使用第二種方法,從45 FPS提升到了53 FPS。你可能會認為這點性能提升并不能讓人影響深刻。但是在接下來的教程中,drawTriangle方法將更加復雜,比如處理陰影和照明之類的,并行處理幾乎可以得到兩倍的性能提升。
我們還可以看看這個并行處理方法分析的核心利用圖:

比較以前的核心圖,你就會明白為什么它更加高效了。
你可以在這里下載含有這種優化的C#版本解決方案:
- C#:[SoftEngineCSharpPart4Bonus.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineCSharpPart4Bonus.zip)
那么,在Html5/JavaScript中為什么不能應用這種方法呢?
Html5在JavaScript中衛開發人員提供了一個新的API用于處理類似的情況。它叫Web Workers,使用它可以處理使用多核心的情況。
David Catuhe和我已經在這三篇話題中討論過這個東西:
- [介紹Html5的Web Workers:JavaScript的多線程方法](http://blogs.msdn.com/b/davrous/archive/2011/07/15/introduction-to-the-html5-web-workers-the-javascript-multithreading-approach.aspx):如果你還不知道Web Workers,那么你應該先閱讀這篇文章
- [使用Web Workers,用于提高圖像處理性能](http://blogs.msdn.com/b/eternalcoding/archive/2012/09/20/using-web-workers-to-improve-performance-of-image-manipulation.aspx):一篇很有意思的文章,我們使用Web Workers來獲得像素操作的性能提升
- [教程系列:在Windows 8中使用WinJS和WinRT構建一個有趣的Html5攝像頭應用程序](http://blogs.msdn.com/b/davrous/archive/2012/10/02/tutorial-series-using-winjs-amp-winrt-to-build-a-fun-html5-camera-application-for-windows-8-4-4.aspx):在這個教程中使用Web Workers來進行攝像頭拍攝的圖像效果處理
我們使用message與workers進行溝通。這意味著大多數時間被用于從UI線程給workers線程傳輸拷貝數據。我們只有很少部分的類型可以使用。事實上,隨著[轉換對象](http://www.w3.org/html/wg/drafts/html/master/infrastructure.html#transferable-objects),當轉移到workers時原始對象將從呼叫者上下文(UI線程)中清除。
但是在我們加速Html5的3D軟件渲染引擎時,這并不是主要的問題。在發送數據的時候將有*memcpy()*進行操作,這是一個非常快速的過程。但真正的問題是,當workers完成了處理后,需要將結果發送回主線程而且這個主線程還需要將數據重新填充回像素數組中。這個操作將很簡單的抵消掉所有Web Workers帶來的性能增益。
所以最后,我還沒有找到一個有效的并行處理方法來實現Html5中3D軟件渲染引擎的性能提升。不過,可能我有一些東西還不知道。如果您可以解決當前Web Workers的局限,獲得到一個顯著的性能提升的話,我非常樂意接受建議! :)
在接下來的教程中,我們將討論[平面著色和高氏著色](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)。我們的實現將開始真正耀眼! :)