譯者前言:
本文譯自[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繪制線條及三角形](http://blog.csdn.net/teajs/article/details/49998675),我們已經開始看到3D網格的線框渲染效果了。但是,我們只顯示了一個立方體……嗯……甚至連一個簡單的立方體都已經有12個面片了!難道我們要手動處理比這更復雜的對象?!天……但愿不是如此。
3D建模有助于3D設計人員和開發人員之間的協作。設計人員可以利用其最喜歡的工具來構建場景或網格(3D Studio Max、Maya、Blender等……)。然后,他將作品導出為開發者可以加載的文件格式。開發者將最終將網格加載進實時3D引擎中。有很多種格式可以這么做。在我們的例子中,將使用Json格式。實際上,David Catuhe已經做了從Blender中導出.babylon后綴的Json格式文件導出器了。我們馬上就可以看到如何解析該文件并顯示在我們可愛的軟件渲染引擎中了!
Blender是一個免費的3D建模軟件,你可以在這里進行下載:[http://www.blender.org/download/get-blender/](http://www.blender.org/download/get-blender/)
你可以用Python編寫Blender的插件,不過我們已經做了一個導出器了。
本章教程是以下系列的一部分:
[1 – 編寫相機、網格和設備對象的核心邏輯](http://blog.csdn.net/teajs/article/details/49989681)
[2 – 繪制線段和三角形來獲得線框渲染效果](http://blog.csdn.net/teajs/article/details/49998675)
3 – 加載通過Blender擴展導出JSON格式的網格(本文)
[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)
通過本章節,你將能夠看到這樣的效果:
[點擊運行](http://david.blob.core.windows.net/softengine3d/part3/index.html)
而且你會發現在前面兩個章節中,你已經完成了大部分工作。
在Blender中安裝Babylon導出器并生成你的場景
如果你已經安裝了Blender,請從這里下載我們的Babylon導出器:[io_export_babylon.py](http://david.blob.core.windows.net/softengine3d/io_export_babylon.py)
將此文件復制到安裝Blender目錄的 \script\addons 目錄下(例如:我本機的目錄就是 "C:\Program Files\Blender Foundation\Blender\2.67\scripts\addons")。
你需要激活我們的插件在用戶首選項中。選擇“文件” -> “用戶首選項”和“擴展中心”選項卡。搜索“babylon”,并勾選激活。

你可以在Blender中做你任何想做的事。如果你像我一樣真的很不善于3D建模,那么這里有一個很酷的選項。直接在菜單欄中選擇“添加” -> “網格” -> “猴子”

然后,你應該可以看到這樣的畫面:

最后一步,將其導出為.babylon文件格式(也就是我們的Json文件)。在菜單欄中選擇 “文件” -> “導出” -> "Babylon.js"

文件名為“monkey.babylon”。
注意:這只猴子名字叫蘇珊妮(Suzanne)并且是3D/游戲社區中非常有名的。通過了解它,你會覺得這個團隊很酷并為此感到驕傲!歡迎加入! ;)
加載導出的Json文件并將其顯示
我將告訴你,在這篇文章的開頭,我們已經建立了所需的所有邏輯,可以顯示更復雜的蘇珊妮網格。我們有面片、網格和頂點的邏輯,這就是我們所需要的了。
Babylone導出器為我們導出了超過我們所需要的數據并存放在了Json文件中。例如:紋理支持、燈光等等。這就是為什么我們在解析的時候直接跳到目前唯一注重的:頂點和面片部分了,因為線框渲染并不需要更多其他數據。
注意:C#開發人員,你需要通過NuGet從Newtonsoft安裝一個Json.Net庫,就像我們第一章中安裝SharpDX一樣。事實上,Json解析并不像瀏覽器中的JavaScript一樣原生支持.Net。
我們先在設備(Device)對象中添加加載邏輯:
【譯者注:C#代碼】
~~~
// 以異步加載方式加載Json文件
public async Task<Mesh[]> LoadJSONFileAsync(string fileName)
{
var meshes = new List<Mesh>();
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 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(一個面片有三個頂點索引)
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;
mesh.Vertices[index] = new Vector3(x, y, z);
}
// 然后填充面片數組
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);
meshes.Add(mesh);
}
return meshes.ToArray();
}
~~~
【譯者注:TypeScript代碼】
~~~
// 以異步加載方式加載Json文件
// 加載完成后向回調函數傳入解析完成的網格
public LoadJSONFileAsync(fileName: string, callback: (result: Mesh[]) => any): void {
var jsonObject = {};
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", fileName, true);
var that = this;
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
jsonObject = JSON.parse(xmlhttp.responseText);
callback(that.CreateMeshesFromJSON(jsonObject));
}
};
xmlhttp.send(null);
}
private CreateMeshesFromJSON(jsonObject): Mesh[] {
var meshes: Mesh[] = [];
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(一個面片有三個頂點索引)
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];
mesh.Vertices[index] = new BABYLON.Vector3(x, y, z);
}
// 然后填充面片數組
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]);
meshes.push(mesh);
}
return meshes;
}
~~~
【譯者注:JavaScript代碼】
~~~
// 以異步加載方式加載Json文件
// 加載完成后向回調函數傳入解析完成的網格
Device.prototype.LoadJSONFileAsync = function (fileName, callback) {
var jsonObject = {};
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", fileName, true);
var that = this;
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
jsonObject = JSON.parse(xmlhttp.responseText);
callback(that.CreateMeshesFromJSON(jsonObject));
}
};
xmlhttp.send(null);
};
Device.prototype.CreateMeshesFromJSON = function (jsonObject) {
var meshes = [];
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(一個面片有三個頂點索引)
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];
mesh.Vertices[index] = new BABYLON.Vector3(x, y, z);
}
// 然后填充面片數組
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]);
meshes.push(mesh);
}
return meshes;
};
~~~
你可能會問,為什么我們要設置6、8、10的步進值?這是因為Babylon增加了更多的細節,我們在使用的時候直接把這些細節過濾掉。
這種邏輯是特定于我們的文件格式的,如果要加載其他(如Three.js)導出器的文件,你只需要實現另一種文件格式的規范讀取點、面和網格。
**注意**:要想能夠載入我們的.babylon文件,對于TypeScript/JavaScript開發者而言,在IIS中,需要在web.config中定義一個新的MIME類型"application/babylon",擴展名為".babylon"。否則將出現404.3錯誤。
~~~
<system.webServer>
<staticContent>
<mimeMap fileExtension=".babylon" mimeType="application/babylon" />
</staticContent>
</system.webServer>
~~~
對于C#開發者來說,你需要更改文件屬性,將其包含在解決方案中,編譯方式為“內容”,并在復制輸出目錄中選擇“始終復制”。

否則,該文件將不會被發現。
最后,我們需要更新我們的主要功能,手動調用LoadJSONFileAsync函數。如果我們加載多個網格動畫,則需要在繪制時旋轉每一個網格:
【譯者注:C#代碼】
~~~
private Device device;
Mesh[] meshes;
Camera mera = new Camera();
private async void Page_Loaded(object sender, RoutedEventArgs e)
{
// 在這里設置后臺緩沖區的分辨率
WriteableBitmap bmp = new WriteableBitmap(640, 480);
// 設置我們的XAML圖像源
frontBuffer.Source = bmp;
device = new Device(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(object sender, object e)
{
device.Clear(0, 0, 0, 255);
foreach (var mesh in meshes)
{
// 每一幀都稍微轉動一下立方體
mesh.Rotation = new Vector3(mesh.Rotation.X + 0.01f, mesh.Rotation.Y + 0.01f, mesh.Rotation.Z);
}
// 做各種矩陣運算
device.Render(mera, meshes);
// 刷新后臺緩沖區到前臺緩沖區
device.Present();
}
~~~
【譯者注:TypeScript代碼】
~~~
///<reference path="SoftEngine.ts"/>
var canvas: HTMLCanvasElement;
var device: SoftEngine.Device;
var meshes: SoftEngine.Mesh[] = [];
var mera: SoftEngine.Camera;
document.addEventListener("DOMContentLoaded", init, false);
function init() {
canvas = <HTMLCanvasElement> document.getElementById("frontBuffer");
mera = new SoftEngine.Camera();
device = new SoftEngine.Device(canvas);
mera.Position = new BABYLON.Vector3(0, 0, 10);
mera.Target = new BABYLON.Vector3(0, 0, 0);
device.LoadJSONFileAsync("monkey.babylon", loadJSONCompleted)
}
function loadJSONCompleted(meshesLoaded: SoftEngine.Mesh[]) {
meshes = meshesLoaded;
// 調用Html5渲染循環
requestAnimationFrame(drawingLoop);
}
// 渲染循環處理
function drawingLoop() {
device.clear();
for (var i = 0; i < meshes.length; i++) {
// 每幀都稍微轉動一下立方體
meshes[i].Rotation.x += 0.01;
meshes[i].Rotation.y += 0.01;
}
// 做各種矩陣運算
device.render(mera, meshes);
// 刷新后臺緩沖區到前臺緩沖區
device.present();
// 遞歸調用Html5渲染循環
requestAnimationFrame(drawingLoop);
}
~~~
【譯者注:JavaScript代碼】
~~~
var canvas;
var device;
var meshes = [];
var mera;
document.addEventListener("DOMContentLoaded", init, false);
function init() {
canvas = document.getElementById("frontBuffer");
mera = new SoftEngine.Camera();
device = new SoftEngine.Device(canvas);
mera.Position = new BABYLON.Vector3(0, 0, 10);
mera.Target = new BABYLON.Vector3(0, 0, 0);
device.LoadJSONFileAsync("monkey.babylon", loadJSONCompleted);
}
function loadJSONCompleted(meshesLoaded) {
meshes = meshesLoaded;
// 調用Html5渲染循環
requestAnimationFrame(drawingLoop);
}
// 渲染循環處理
function drawingLoop() {
device.clear();
for (var i = 0; i < meshes.length; i++) {
// 每幀都稍微轉動一下立方體
meshes[i].Rotation.x += 0.01;
meshes[i].Rotation.y += 0.01;
}
// 做各種矩陣運算
device.render(mera, meshes);
// 刷新后臺緩沖區到前臺緩沖區
device.present();
// 遞歸調用Html5渲染循環
requestAnimationFrame(drawingLoop);
}
~~~
你現在應該有一個3D引擎,它可以加載一個由Blender導出的網格文件并且以線框模式渲染了還有動畫!雖然我不知道你現在的感覺,但我還是很高興能夠到達這個階段。 :)
如果沒有,下載源代碼:
C#:[SoftEngineCSharpPart3.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineCSharpPart3.zip)
TypeScript:[SoftEngineTSPart3.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineTSPart3.zip)
JavaScript:[SoftEngineJSPart3.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineJSPart3.zip)?或只需右鍵點擊 -> 查看框架的源代碼
那么,接下來會發生些什么呢?好了,我們需要填充三角形。這就是所謂的光柵化。我們也將使用深度緩沖區用來實現正確的渲染效果。在接下來的教程中,你將會了解如何獲得這樣的效果:
