💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
译者前言: 本文译自[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”,并勾选激活。 ![激活插件](https://box.kancloud.cn/2016-03-22_56f0e989230a5.jpg "激活插件") 你可以在Blender中做你任何想做的事。如果你像我一样真的很不善于3D建模,那么这里有一个很酷的选项。直接在菜单栏中选择“添加” -> “网格” -> “猴子” ![添加网格](https://box.kancloud.cn/2016-03-22_56f0e989358e3.jpg "添加网格") 然后,你应该可以看到这样的画面: ![Blender建模](https://box.kancloud.cn/2016-03-22_56f0e9895c8ed.jpg "Blender建模") 最后一步,将其导出为.babylon文件格式(也就是我们的Json文件)。在菜单栏中选择 “文件” -> “导出” -> "Babylon.js" ![导出Babylon](https://box.kancloud.cn/2016-03-22_56f0e98981833.jpg "导出Babylon") 文件名为“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#开发者来说,你需要更改文件属性,将其包含在解决方案中,编译方式为“内容”,并在复制输出目录中选择“始终复制”。 ![文件属性](https://box.kancloud.cn/2016-03-22_56f0e9899e2c6.jpg "文件属性") 否则,该文件将不会被发现。 最后,我们需要更新我们的主要功能,手动调用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) 或只需右键点击 -> 查看框架的源代码 那么,接下来会发生些什么呢?好了,我们需要填充三角形。这就是所谓的光栅化。我们也将使用深度缓冲区用来实现正确的渲染效果。在接下来的教程中,你将会了解如何获得这样的效果: ![光栅化与深度缓冲](https://box.kancloud.cn/2016-03-22_56f0e989b1286.jpg "光栅化与深度缓冲")