🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
译者前言: 本文译自[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游戏!一探究竟。它是免费并且充满乐趣的。 [![](https://box.kancloud.cn/2016-03-22_56f0e98b539b1.jpg)](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个三角形。 下面这张图片将简单的帮助你理解: ![](https://box.kancloud.cn/2016-03-22_56f0e98b7d586.jpg) 我们可以用非常类似的方法顺利的在3D引擎中工作。想象一下,这个图像将作为我们的立方体纹理被应用。认为它是一个颜色字节的二维数组。我们可以使用2D坐标将该阵列中的值移动到每个立方体的顶点,以此来获得类似的东西: ![](https://box.kancloud.cn/2016-03-22_56f0e98ba254f.jpg) 此图片来源于:[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个字母排列)。我期待一个更复杂的答案!![](https://box.kancloud.cn/2016-03-22_56f0e98bbe0df.gif) 现在你可能会问自己如何处理苏珊妮,我们的美丽的猴子脑袋,不是吗? 对于这种网状的,我们也将使用3D映射一个单一的2D图像。要建立相应的纹理,我们需要您计划2D网格视图。此操作被称为展开(unwrap)操作。如果您是一个可怜的开发者比如我,相信我,你需要一个像我的朋友[Michel Rousseau](http://blogs.msdn.com/designmichel)一样的3D设计师,在这个阶段来帮助您!而这也正是我都做了什么:寻求帮助。![](https://box.kancloud.cn/2016-01-18_569ca4488de4a.gif) 使用苏珊妮作为一个例子,在展开操作之后,设计者将会得到这样的结果: ![](https://box.kancloud.cn/2016-03-22_56f0e98bd3832.jpg) 那么设计师会在这样一个二维视图中画画以便于在我们的引擎中使用纹理。在本例中,Michel Rousseau来做这项工作,这里是他自己的苏珊妮版本: ![](https://box.kancloud.cn/2016-03-22_56f0e98be840f.jpg) 我知道第一次去试着理解纹理映射这个结果显得比较奇怪。但是,你应该已经可以看到一些东西,看起来纹理右下角是眼睛。这部分将在使用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),请阅读第一部分这一定会帮助你至少知道如何映射到一个立方体。 ![](https://box.kancloud.cn/2016-03-02_56d6ad910ce86.gif) ![](https://box.kancloud.cn/2016-03-22_56f0e98c20771.jpg) 代码 现在,我们准备开发新代码了。我们将有几项工作要完成: 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; }; ~~~ 有了这些修改,我们现在可以看到这个使用高氏着色算法渲染出美丽的苏珊妮模型了: [![](https://box.kancloud.cn/2016-03-22_56f0e98c3c3d0.jpg)](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。极少数应该被绘制的三角形没有被绘制。这是因为我们应该调整法线的变换,以便考虑到摄像机当前视角。当前算法使得我们有了一个正交相机,但却有些不同。解决这个问题对你来说应该是一个很好的锻炼!![](https://box.kancloud.cn/2016-02-17_56c446a99dec4.gif) 提升性能是很有意思的事,我们大约得到了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的设备/浏览器的话,你可以在这里测试效果: ![](https://box.kancloud.cn/2016-03-22_56f0e98c5fa50.jpg) 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帧每秒的速度! 本系列教程已完结。