譯者前言:
本文譯自[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/),文章中如果有我的額外說明,我會加上【譯者注:】。
正文開始:
下面是本系列的最后一個章節了。我們將看到如何從Blender中導出貼圖和紋理坐標來使我們的網格應用紋理。如果你已經成功的了解了之前的教程,應用一些紋理對你來說應該是小菜一碟。主要概念依舊是在每個頂點間插補一些數據。在本章的第二部分中,我們將看到如何提高我們的渲染算法性能。為此,我們將使用背面剔除來使得只有我們能看到的部分被繪制。但是更進一步,我們會用最后的秘密武器:GPU。那么你將會明白為什么OpenGL/WebGL和DirectX這些技術對實時3D游戲非常重要。它們有助于利用GPU而不是CPU來渲染我們的3D對象。想要真正的看到差異,我們將在加載一個名為Babylon.js的WebGL 3D引擎中使用完全相同的模型JSON文件。渲染FPS將會好的多,尤其是在低端設備!
在本教程的最后,你將可以在自己的3D軟件渲染引擎中看到這樣的渲染效果:
[點擊運行](https://david.blob.core.windows.net/softengine3d/part6/index.html)
本章教程是以下系列的一部分:
[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 – 額外章節:使用技巧和并行處理來提高性能](http://blog.csdn.net/teajs/article/details/50054509)
[5 – 使用平面著色和高氏著色處理光](http://blog.csdn.net/teajs/article/details/50103367)
6 – 應用紋理、背面剔除以及一些WebGL相關 (本文)
進一步深入:我和David Catuhe做了一個免費的擁有8個單元的課程供你學習基礎3D知識,比如WebGL和[Babylon.js](http://www.babylonjs.com/)。第一個模塊是包含本系列教程的40分鐘的視頻版本:[介紹WebGL 3D、Html5和Babylon.js](http://www.microsoftvirtualacademy.com/training-courses/introduction-to-webgl-3d-with-html5-and-babylon-js)。你將學習到很多關于如何使用WebGL制作瀏覽器中運行的3D游戲!一探究竟。它是免費并且充滿樂趣的。
[](http://www.microsoftvirtualacademy.com/training-courses/introduction-to-webgl-3d-with-html5-and-babylon-js)
紋理映射
**概念**
首先讓我們從維基百科的定義開始:[紋理映射](http://en.wikipedia.org/wiki/Texture_mapping):“紋理貼圖應用(映射)到形狀或多邊形表面”。這個過程類似于應用一個圖案到一個純白色的盒子上。在一個多邊形的每個頂點分配一個紋理坐標(在2D情況下也被稱為UV坐標),通過顯式分配或由程序定義。在圖像中的一個位置上取樣然后貼在一個多邊形上,以此產生視覺效果。
讓我們試著去了解它的準確意思。
第一次我試圖想象我們如何能夠將紋理應用到一個立方體3D網格。然后我想拍攝圖像充當我們的紋理并將其映射到每個立方體的面。這可以在一個簡單的例子中工作的很好。但是,第一個問題是:如果我希望每個立方體面應用不同的圖像/紋理該怎么辦?第一個想法是采取6種不同的圖像在不同的6個面。為了更加精確,取6幅圖像,將他們分割成被映射到一個立方體的12個三角形中的2個三角形。
下面這張圖片將簡單的幫助你理解:

我們可以用非常類似的方法順利的在3D引擎中工作。想象一下,這個圖像將作為我們的立方體紋理被應用。認為它是一個顏色字節的二維數組。我們可以使用2D坐標將該陣列中的值移動到每個立方體的頂點,以此來獲得類似的東西:

此圖片來源于:[Texturing a cube in Blender, and going to pull my hair out](http://www.sluniverse.com/php/vb/content-creation/65233-texturing-cube-blender-going-pull.html)
這些2D紋理坐標被稱為UV坐標。
**注**:我詢問了一個3D大神為什么它們被稱為U和V呢?答案是令人驚訝而且顯而易見的:”嗯,因為它們在X、Y、Z之前“(26個字母排列)。我期待一個更復雜的答案!
現在你可能會問自己如何處理蘇珊妮,我們的美麗的猴子腦袋,不是嗎?
對于這種網狀的,我們也將使用3D映射一個單一的2D圖像。要建立相應的紋理,我們需要您計劃2D網格視圖。此操作被稱為展開(unwrap)操作。如果您是一個可憐的開發者比如我,相信我,你需要一個像我的朋友[Michel Rousseau](http://blogs.msdn.com/designmichel)一樣的3D設計師,在這個階段來幫助您!而這也正是我都做了什么:尋求幫助。
使用蘇珊妮作為一個例子,在展開操作之后,設計者將會得到這樣的結果:

那么設計師會在這樣一個二維視圖中畫畫以便于在我們的引擎中使用紋理。在本例中,Michel Rousseau來做這項工作,這里是他自己的蘇珊妮版本:

我知道第一次去試著理解紋理映射這個結果顯得比較奇怪。但是,你應該已經可以看到一些東西,看起來紋理右下角是眼睛。這部分將在使用3D簡單對稱區分操作映射到蘇珊妮的兩個眼睛。
你現在知道了紋理貼圖的基本知識。要明確了解它是如何工作的,請閱讀我在網絡上找到的這些額外資源:
-?[教程16 - 紋理映射基礎](http://ogldev.atspace.co.uk/www/tutorial16/tutorial16.html),請閱讀第一部分,這將有助于你了解如何將UV坐標映射(在0-1之間)在我們的網格三角形
-?[Blender 2.6 手冊](http://wiki.blender.org/index.php/Doc:2.6/Manual/Textures/Mapping/UV/Unwrapping)?- 網格的UV貼圖,描述各種映射類型
-?[教程5 - 紋理映射](http://www.real3dtutorials.com/tut00005.php),請閱讀第一部分這一定會幫助你至少知道如何映射到一個立方體。


代碼
現在,我們準備開發新代碼了。我們將有幾項工作要完成:
1 - 創建一個紋理類將圖像加載進來充當紋理并根據UV坐標返回每像素插值坐標的顏色
2 - 添加/傳遞 在合成渲染流程中的紋理信息
3 - 解析由[Blender的 Babylon導出插件](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)導出的JSON文件去加載UV坐標
紋理邏輯
在Html5和TypeScript/JavaScript中,我們當然要通過動態創建一個Canvas元素加載材質并得到它相關的圖像數據,以用來獲得我們的顏色字節數組。
而在C#/XAML中,我們要創建一個WriteableBitmap,設置源并在加載完圖像后獲得其緩沖區屬性以此獲取我們的顏色字節數組。
【譯者注:C#代碼】
~~~
public class Texture
{
private byte[] internalBuffer;
private int width;
private int height;
// 材質尺寸需要是2的次方(如:512x512、1024x1024等)
public Texture(string filename, int width, int height)
{
this.width = width;
this.height = height;
Load(filename);
}
async void Load(string filename)
{
var file = await Windows.ApplicationModel.Package.Current.InstalledLocation.GetFileAsync(filename);
using (var stream = await file.OpenReadAsync())
{
var bmp = new WriteableBitmap(width, height);
bmp.SetSource(stream);
internalBuffer = bmp.PixelBuffer.ToArray();
}
}
// 獲得Blender導出的UV坐標并將其對應的像素顏色返回
public Color4 Map(float tu, float tv)
{
// 圖像尚未加載
if (internalBuffer == null)
{
return Color4.White;
}
// 使用%運算符來循環/重復需要的這個紋理
int u = Math.Abs((int) (tu*width) % width);
int v = Math.Abs((int) (tv*height) % height);
int pos = (u + v * width) * 4;
byte b = internalBuffer[pos];
byte g = internalBuffer[pos + 1];
byte r = internalBuffer[pos + 2];
byte a = internalBuffer[pos + 3];
return new Color4(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f);
}
}
~~~
【譯者注:TypeScript代碼】
~~~
export class Texture {
width: number;
height: number;
internalBuffer: ImageData;
// 材質尺寸需要是2的次方(如:512x512、1024x1024等)
constructor(filename: string, width: number, height: number) {
this.width = width;
this.height = height;
this.load(filename);
}
public load(filename: string): void {
var imageTexture = new Image();
imageTexture.height = this.height;
imageTexture.width = this.width;
imageTexture.onload = () => {
var internalCanvas: HTMLCanvasElement = document.createElement("canvas");
internalCanvas.width = this.width;
internalCanvas.height = this.height;
var internalContext: CanvasRenderingContext2D = internalCanvas.getContext("2d");
internalContext.drawImage(imageTexture, 0, 0);
this.internalBuffer = internalContext.getImageData(0, 0, this.width, this.height);
};
imageTexture.src = filename;
}
// 獲得Blender導出的UV坐標并將其對應的像素顏色返回
public map(tu: number, tv: number): 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];
return new BABYLON.Color4(r / 255.0, g / 255.0, b / 255.0, a / 255.0);
}
// 圖像尚未加載
else {
return new BABYLON.Color4(1, 1, 1, 1);
}
}
}
~~~
【譯者注:JavaScript代碼】
~~~
var Texture = (function () {
// 材質尺寸需要是2的次方(如:512x512、1024x1024等)
function Texture(filename, width, height) {
this.width = width;
this.height = height;
this.load(filename);
}
Texture.prototype.load = function (filename) {
var _this = this;
var imageTexture = new Image();
imageTexture.height = this.height;
imageTexture.width = this.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 = filename;
};
// 獲得Blender導出的UV坐標并將其對應的像素顏色返回
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];
return new BABYLON.Color4(r / 255.0, g / 255.0, b / 255.0, a / 255.0);
}
// 圖像尚未加載
else {
return new BABYLON.Color4(1, 1, 1, 1);
}
};
return Texture;
})();
SoftEngine.Texture = Texture;
~~~
傳遞紋理信息流程
我不會深入到每一個細節,下面有完整的代碼下載,讓我們來看看你都需要做些什么:
- 添加一個紋理屬性到Mesh類和一個Vector2屬性名稱為TextureCoordinates的Vertex結構
- 更新ScanLineData中嵌入8個單精度小數/數字:每個頂點的UV坐標(ua, ub, uc, ud和va, vb, vc, vd)。
- 更新Project方法/函數返回一個新的Vertex和TextureCoordinates原封不動的使用(傳遞)
- 傳遞一個Texture對象作為DrawTriangle方法/函數的最后一個參數到ProcessScanLine
- 填充新的ScanLineData結構在drawTriangle和相應的UV坐標
-?把UV插值到ProcessScanLine函數的Y上得到SU/SV和EU/EV(start U/start V end U/End V)然后插值U,V在X上,就可以找到它在紋理中對應的顏色。將此顏色與對象本身的顏色(在本教程中一般它是白色)還有法線進行NDotL操作得到光量并進行混合。
**注**:我們的Project方法可以被看作是我們命名為“Vertex Shader”的3D硬件引擎,并且ProcessScanLine可以被看作是"Pixel Shader"引擎。
這篇文章僅在ProcessScanLine中有新的更新部分:
【譯者注:C#代碼】
~~~
void ProcessScanLine(ScanLineData data, Vertex va, Vertex vb, Vertex vc, Vertex vd, Color4 color, Texture texture)
{
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
var gradient1 = pa.Y != pb.Y ? (data.currentY - pa.Y) / (pb.Y - pa.Y) : 1;
var gradient2 = pc.Y != pd.Y ? (data.currentY - pc.Y) / (pd.Y - pc.Y) : 1;
int sx = (int)Interpolate(pa.X, pb.X, gradient1);
int ex = (int)Interpolate(pc.X, pd.X, gradient2);
// 開始Z值和結束Z值
float z1 = Interpolate(pa.Z, pb.Z, gradient1);
float z2 = Interpolate(pc.Z, pd.Z, gradient2);
// 將法線插值到Y中
var snl = Interpolate(data.ndotla, data.ndotlb, gradient1);
var enl = Interpolate(data.ndotlc, data.ndotld, gradient2);
// 將紋理坐標插值到Y中
var su = Interpolate(data.ua, data.ub, gradient1);
var eu = Interpolate(data.uc, data.ud, gradient2);
var sv = Interpolate(data.va, data.vb, gradient1);
var ev = Interpolate(data.vc, data.vd, gradient2);
// 從左(sx)向右(ex)繪制一條線
for (var x = sx; x < ex; x++)
{
float gradient = (x - sx) / (float)(ex - sx);
// 將Z坐標、法線和紋理坐標插值到X中
var z = Interpolate(z1, z2, gradient);
var ndotl = Interpolate(snl, enl, gradient);
var u = Interpolate(su, eu, gradient);
var v = Interpolate(sv, ev, gradient);
Color4 textureColor;
if (texture != null)
textureColor = texture.Map(u, v);
else
textureColor = new Color4(1, 1, 1, 1);
// 使用光向量、法線向量的角度余弦值以及材質顏色來改變原本顏色值
DrawPoint(new Vector3(x, data.currentY, z), color * ndotl * textureColor);
}
}
~~~
【譯者注:TypeScript代碼】
~~~
public processScanLine(data: ScanLineData, va: Vertex, vb: Vertex, vc: Vertex, vd: Vertex, color: BABYLON.Color4, texture?: Texture): 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 gradient1 = pa.y != pb.y ? (data.currentY - pa.y) / (pb.y - pa.y) : 1;
var gradient2 = pc.y != pd.y ? (data.currentY - pc.y) / (pd.y - pc.y) : 1;
var sx = this.interpolate(pa.x, pb.x, gradient1) >> 0;
var ex = this.interpolate(pc.x, pd.x, gradient2) >> 0;
// 開始Z值和結束Z值
var z1: number = this.interpolate(pa.z, pb.z, gradient1);
var z2: number = this.interpolate(pc.z, pd.z, gradient2);
// 將法線插值到Y中
var snl = this.interpolate(data.ndotla, data.ndotlb, gradient1);
var enl = this.interpolate(data.ndotlc, data.ndotld, gradient2);
// 將紋理坐標插值到Y中
var su = this.interpolate(data.ua, data.ub, gradient1);
var eu = this.interpolate(data.uc, data.ud, gradient2);
var sv = this.interpolate(data.va, data.vb, gradient1);
var ev = this.interpolate(data.vc, data.vd, gradient2);
// 從左(sx)向右(ex)繪制一條線
for (var x = sx; x < ex; x++) {
var gradient: number = (x - sx) / (ex - sx);
// 將Z坐標、法線和紋理坐標插值到X中
var z = this.interpolate(z1, z2, gradient);
var ndotl = this.interpolate(snl, enl, gradient);
var u = this.interpolate(su, eu, gradient);
var v = this.interpolate(sv, ev, gradient);
var textureColor;
if (texture)
textureColor = texture.map(u, v);
else
textureColor = new BABYLON.Color4(1, 1, 1, 1);
// 使用光向量、法線向量的角度余弦值以及材質顏色來改變原本顏色值
this.drawPoint(new BABYLON.Vector3(x, data.currentY, z),
new BABYLON.Color4(color.r * ndotl * textureColor.r,
color.g * ndotl * textureColor.g,
color.b * ndotl * textureColor.b, 1));
}
}
~~~
【譯者注:JavaScript代碼】
~~~
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 gradient1 = pa.y != pb.y ? (data.currentY - pa.y) / (pb.y - pa.y) : 1;
var gradient2 = pc.y != pd.y ? (data.currentY - pc.y) / (pd.y - pc.y) : 1;
var sx = this.interpolate(pa.x, pb.x, gradient1) >> 0;
var ex = this.interpolate(pc.x, pd.x, gradient2) >> 0;
// 開始Z值和結束Z值
var z1 = this.interpolate(pa.z, pb.z, gradient1);
var z2 = this.interpolate(pc.z, pd.z, gradient2);
// 將法線插值到Y中
var snl = this.interpolate(data.ndotla, data.ndotlb, gradient1);
var enl = this.interpolate(data.ndotlc, data.ndotld, gradient2);
// 將紋理坐標插值到Y中
var su = this.interpolate(data.ua, data.ub, gradient1);
var eu = this.interpolate(data.uc, data.ud, gradient2);
var sv = this.interpolate(data.va, data.vb, gradient1);
var ev = this.interpolate(data.vc, data.vd, gradient2);
// 從左(sx)向右(ex)繪制一條線
for (var x = sx; x < ex; x++) {
var gradient = (x - sx) / (ex - sx);
// 將Z坐標、法線和紋理坐標插值到X中
var z = this.interpolate(z1, z2, gradient);
var ndotl = this.interpolate(snl, enl, gradient);
var u = this.interpolate(su, eu, gradient);
var v = this.interpolate(sv, ev, gradient);
var textureColor;
if (texture)
textureColor = texture.map(u, v);
else
textureColor = new BABYLON.Color4(1, 1, 1, 1);
// 使用光向量、法線向量的角度余弦值以及材質顏色來改變原本顏色值
this.drawPoint(new BABYLON.Vector3(x, data.currentY, z),
new BABYLON.Color4(color.r * ndotl * textureColor.r,
color.g * ndotl * textureColor.g,
color.b * ndotl * textureColor.b, 1));
}
};
~~~
如果你已經按照之前的所有教程建立過自己的版本,那么請下載我的解決方案進行對比并更新你的項目。
從Babylon JSON文件格式中載入信息
為了能夠很好的渲染你在本章最開始所看到的效果,你需要加載由[Michel Rousseau](https://twitter.com/rousseau_michel)所修改的貼圖以及從Blender導出的蘇珊妮模型的最新版本。因此,請下載這兩個文件:
- 從Blender中導出的蘇珊妮模型以及紋理坐標集合:
[http://david.blob.core.windows.net/softengine3d/part6/monkey.babylon](http://david.blob.core.windows.net/softengine3d/part6/monkey.babylon)
- 要加載的512x512大小的紋理貼圖:
[http://david.blob.core.windows.net/softengine3d/part6/Suzanne.jpg](http://david.blob.core.windows.net/softengine3d/part6/Suzanne.jpg)
David Catuhe所導出的Babylon.JSON格式文件包含了很多我們本章節不會涵蓋的細節。例如,貼材質到模型上。實際上,設計者可以使用特殊材質來貼到一個網格中。在我們的例子中,我們只打算處理漫反射紋理。如果你想要實現更多,還請以David Catuhe的文章作為基礎:[Babylon.js:在您的游戲中使用標準材質](http://blogs.msdn.com/b/eternalcoding/archive/2013/07/01/babylon-js-unleash-the-standardmaterial-for-your-babylon-js-game.aspx)
接下來,我將只與你分享有變化的主體部分:加載和分析JSON文件的方法/函數。
~~~
// 以異步方式加載JSON文件
public async Task<Mesh[]> LoadJSONFileAsync(string fileName)
{
var meshes = new List<Mesh>();
var materials = new Dictionary<String,Material>();
var file = await Windows.ApplicationModel.Package.Current.InstalledLocation.GetFileAsync(fileName);
var data = await Windows.Storage.FileIO.ReadTextAsync(file);
dynamic jsonObject = Newtonsoft.Json.JsonConvert.DeserializeObject(data);
for (var materialIndex = 0; materialIndex < jsonObject.materials.Count; materialIndex++)
{
var material = new Material();
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;
materials.Add(material.ID, material);
}
for (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)
{
case 0:
verticesStep = 6;
break;
case 1:
verticesStep = 8;
break;
case 2:
verticesStep = 10;
break;
}
// 有趣的頂點信息數字
var verticesCount = verticesArray.Count / verticesStep;
// 面數是邏輯上的大小除以3(A,B,C)得到
var facesCount = indicesArray.Count / 3;
var mesh = new Mesh(jsonObject.meshes[meshIndex].name.Value, verticesCount, facesCount);
// 首先我們填充網格的頂點數組
for (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] = new Vertex
{
Coordinates = new Vector3(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);
}
}
// 然后填充模型面數組
for (var index = 0; index < facesCount; 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 = new Texture(meshTextureName, 512, 512);
}
meshes.Add(mesh);
}
return meshes.ToArray();
}
~~~
【譯者注:TypeScript代碼】
~~~
private CreateMeshesFromJSON(jsonObject): Mesh[] {
var meshes: Mesh[] = [];
var materials: Material[] = [];
for (var materialIndex = 0; materialIndex < jsonObject.materials.length; materialIndex++) {
var material: 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;
materials[material.ID] = material;
}
for (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) {
case 0:
verticesStep = 6;
break;
case 1:
verticesStep = 8;
break;
case 2:
verticesStep = 10;
break;
}
// 有趣的頂點信息數字
var verticesCount = verticesArray.length / verticesStep;
// 面數是邏輯上的大小除以3(A,B,C)得到
var facesCount = indicesArray.length / 3;
var mesh = new SoftEngine.Mesh(jsonObject.meshes[meshIndex].name, verticesCount, facesCount);
// 首先我們填充網格的頂點數組
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] = {
Coordinates: new BABYLON.Vector3(x, y, z),
Normal: new 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);
}
else {
mesh.Vertices[index].TextureCoordinates = new BABYLON.Vector2(0, 0);
}
}
// 然后填充模型面數組
for (var index = 0; index < facesCount; 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 = new Texture(meshTextureName, 512, 512);
}
meshes.push(mesh);
}
return meshes;
}
~~~
【譯者注:JavaScript代碼】
~~~
Device.prototype.CreateMeshesFromJSON = function (jsonObject) {
var meshes = [];
var materials = [];
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;
materials[material.ID] = material;
}
for (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) {
case 0:
verticesStep = 6;
break;
case 1:
verticesStep = 8;
break;
case 2:
verticesStep = 10;
break;
}
// 有趣的頂點信息數字
var verticesCount = verticesArray.length / verticesStep;
// 面數是邏輯上的大小除以3(A,B,C)得到
var facesCount = indicesArray.length / 3;
var mesh = new SoftEngine.Mesh(jsonObject.meshes[meshIndex].name, verticesCount, facesCount);
// 首先我們填充網格的頂點數組
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] = {
Coordinates: new BABYLON.Vector3(x, y, z),
Normal: new 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);
}
else {
mesh.Vertices[index].TextureCoordinates = new BABYLON.Vector2(0, 0);
}
}
// 然后填充模型面數組
for (var index = 0; index < facesCount; 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 = new Texture(meshTextureName, 512, 512);
}
meshes.push(mesh);
}
return meshes;
};
~~~
有了這些修改,我們現在可以看到這個使用高氏著色算法渲染出美麗的蘇珊妮模型了:
[](http://david.blob.core.windows.net/softengine3d/part6sample1/index.html)
3D軟件渲染引擎:[在瀏覽器中使用Html5查看蘇珊妮紋理和高氏著色示例](http://david.blob.core.windows.net/softengine3d/part6sample1/index.html)
你可以在這里下載執行這一紋理映射算法解決方案:
- C#:?[SoftEngineCSharpPart6Sample1.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineCSharpPart6Sample1.zip)
- TypeScript:?[SoftEngineTSPart6Sample1.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineTSPart6Sample1.zip)
- JavaScript:?[SoftEngineJSPart6Sample1.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineJSPart6Sample1.zip)
性能差異不會很大。在我的機器中C#版本用1600x900的分辨率以18幀每秒的速度運行,而 用Html5版本用640x480的分辨率以15幀每秒的速度運行在IE11中。
在使用GPU方案之前,讓我們來看看你的3D軟件渲染引擎最終優化方案。
背面剔除
讓我們再次從維基百科的定義開始:[背面剔除](http://en.wikipedia.org/wiki/Back-face_culling):”在[計算機圖形學](http://en.wikipedia.org/wiki/Computer_graphics)中,背面剔除用來確定圖形對象的[多邊形](http://en.wikipedia.org/wiki/Polygon)是否可見實施背面剔除的一種方法是通過丟棄其[表面法線](http://en.wikipedia.org/wiki/Surface_normal)和相機到多邊形向量的[點積](http://en.wikipedia.org/wiki/Dot_product)大于或等于零的所有多邊形“。
這個想法是我們的例子中在每個網格表面法線預計算的時候,在加載并解析階段使用之前教程中的平面著色相同的算法來完成。一旦這樣做,在Render方法/函數,我們將改變表面法線在世界視圖(相機觀看世界)的坐標,并檢查它的Z值。如果它>=0,意味著我們不會繪制這個三角形,這個模型面在鏡頭中不可見。
3D軟件渲染引擎:[在瀏覽器中使用Html5查看蘇珊妮紋理、高氏著色以及啟用了背面剔除的示例](http://david.blob.core.windows.net/softengine3d/part6sample2/index.html)
你可以在這里下載執行這一背面剔除解決方案:
- C#:?[SoftEngineCSharpPart6Sample2.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineCSharpPart6Sample2.zip)
- TypeScript:?[SoftEngineTSPart6Sample2.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineTSPart6Sample2.zip)
- JavaScript:?[SoftEngineJSPart6Sample2.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineJSPart6Sample2.zip)
**注**:你會發現我的背面剔除方案有一些小Bug。極少數應該被繪制的三角形沒有被繪制。這是因為我們應該調整法線的變換,以便考慮到攝像機當前視角。當前算法使得我們有了一個正交相機,但卻有些不同。解決這個問題對你來說應該是一個很好的鍛煉!
提升性能是很有意思的事,我們大約得到了66%的性能提升,我在IE11環境下從平均每秒15幀轉向啟用背面剔除,從而提升到了25幀每秒。
通過Babylon.js使用WebGL進行渲染
如今的3D游戲,理所應當要使用GPU來渲染。本系列教程的真正目的是建立自己的3D軟件渲染引擎,了解3D相關的基本知識。一旦你能明白這幾章的內容,使用OpenGL/WebGL或DirectX將會是得心應手的。
在我們周圍,一直都有一套框架可以使得開發人員非常容易的構建Html5 3D游戲。它就是由David Catuhe構建的Babylon.js。
David已經開始在他的博客上寫了一系列教程以便讓大家知道如何使用他自己寫的WebGL 3D引擎。入口在這里:[Babylon.js:使用JavaScript的Html5 WebGL引擎來構建3D游戲](http://blogs.msdn.com/b/eternalcoding/archive/2013/06/27/babylon-js-a-complete-javascript-framework-for-building-3d-games-with-html-5-and-webgl.aspx)
通過這一系列教程:[Babylon.js:如何加載并使用由Blender導出的.babaylon文件](http://blogs.msdn.com/b/eternalcoding/archive/2013/06/28/babylon-js-how-to-load-a-babylon-file-produced-with-blender.aspx),你就可以在瀏覽器中使用GPU來加速我們的模型!
如果你由IE11,Chrome或FireFox或任何可以執行WebGL的設備/瀏覽器的話,你可以在這里測試效果:

Babylon.js - WebGL的3D引擎:[預覽蘇珊娜模型紋理以及硬件加速效果!](http://david.blob.core.windows.net/softengine3d/part6webgl/index.html)
由于使用了WebGL,我們得到了一個巨大的性能提升。比如,在我的Surface RT的Windows8.1中,使用IE11,我從3D軟件渲染引擎的640x480分辨率繪制4幀每秒的速度提升到了WebGL渲染引擎的1366x768分辨率下的60幀每秒的速度!
本系列教程已完結。