译者前言:
本文译自[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/),文章中如果有我的额外说明,我会加上【译者注:】。
正文开始:
![效果预览](https://box.kancloud.cn/2016-03-22_56f0e987d9ebe.jpg "效果预览")
我非常乐意通过一系列教程和大家分享如何建立所谓的”3D软件渲染引擎“。
”软件渲染引擎“意味着我们将只使用CPU建立一个3D引擎,完全不使用GPU。(请不要忘记毁灭你的80386?)
我将与你分享C#、TypeScript以及JavaScript三种版本的代码。在这三者中,你应该可以找到自己喜欢的语言,或者至少比较接近你所喜欢的语言。
这样做是为了让你更容易从语法中抽身,把重点放在概念和实现上以及你最喜欢的编程环境中。
代码在文章末尾可以下载到。
那么,为什么要建立一个3D软件渲染引擎呢?嗯……这只是因为它确实有助于理解现代化的3D实现以及GPU处理器原理。
事实上,我非常感谢[David Catuhe](http://blogs.msdn.com/b/eternalcoding/)在微软内部研讨会中提供的3D基础知识。他已经掌握3D知识非常之久,矩阵运算就像是硬编码一样刻在了他的脑子里。我在年轻的时候,就一直梦想着能写这样的引擎,但是这对我来说太复杂了。但是最后,你会发现这并不复杂。你只是需要一个人帮你理解基本原理以及最简单的实现方式。
通过本系列教程,你将学习到如何在2D屏幕中绘制一些3D顶点(X,Y,Z)、如何绘制每个点之间的线段、如何填充一些三角形、处理灯光、材质等。
这第一篇教程就先只告诉你如何显示8个顶点组成一个立方体以及如何在3D虚拟世界中进行移动。
本章教程是以下系列的一部分:
1 – 编写相机、网格和设备对象的核心逻辑(本文)
[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://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)
如果按照完整的系列教程学习,你就会**知道如何建立自己的3D软件渲染引擎**!引擎将会描绘一些线框,然后光栅化,再进行高氏着色,最后应用纹理。
[![绘制线框](https://box.kancloud.cn/2016-03-22_56f0e987f01ed.jpg "绘制线框")](http://david.blob.core.windows.net/html5/SoftEngineProgression/wireframe/index.html)[![光栅化](https://box.kancloud.cn/2016-03-22_56f0e988112af.jpg "光栅化")](http://david.blob.core.windows.net/html5/SoftEngineProgression/rasterization/index.html)[![高氏着色](https://box.kancloud.cn/2016-03-22_56f0e9882a18b.jpg "高氏着色")](http://david.blob.core.windows.net/html5/SoftEngineProgression/gouraudshading/index.html)[![应用纹理](https://box.kancloud.cn/2016-03-22_56f0e9883b64d.jpg "应用纹理")](http://david.blob.core.windows.net/html5/SoftEngineProgression/textured/index.html)
点击图片查看阶段演示,在本系列教程中我们将讨论如何从线框到最终的纹理。
点击[运行代码](http://david.blob.core.windows.net/softengine3d/part1/index.html)后,你将看到学习本章节将会实现8个点的效果。
**免责声明**:
出于教学目的,所以我要建立一个这样的3D软件渲染引擎,而不是使用GPU来渲染。
当然,如果你需要建立一个游戏流体3D动画,你需要的是DirectX或OpenGL/WebGL之类的。
但是,一但你明白如何建立一个3D软件渲染引擎,那么更”复杂“的引擎将更容易理解。
再进一步,你绝对应该看看由David Catuhe建立的[BabylonJS WebGL引擎](http://www.babylonjs.com/)。
更多详情及教程在这里:[Babylon.js,使用Html5和WebGL的一个完整JavaScript框架来构建游戏。](http://www.microsoftvirtualacademy.com/training-courses/introduction-to-webgl-3d-with-html5-and-babylon-js)
**查看MVA视频培训版本**:我们已经与David Catuhe做了一个免费的8个单元的课程,让你学习到基本3D知识、WebGL和[Babylon.js](http://www.babylonjs.com/)。
第一单元是包含本系列教程的一个40分钟的视频版本:[介绍Html5 WebGL 3D和Babylon.js](http://www.microsoftvirtualacademy.com/training-courses/introduction-to-webgl-3d-with-html5-and-babylon-js)
[![视频教学](https://box.kancloud.cn/2016-03-22_56f0e9884f52b.jpg "视频教学")](https://mva.microsoft.com/en-US/training-courses/introduction-to-webgl-3d-with-html5-and-babylon-js-8421)
**阅读前提条件**
我用了很长一段时间一直在思考如何些这些教程。现在我终于决定不再解释每个必须了解的原理。
在网络上有很多不错的资源,比我能更好的解释这些关键原理。
但我也花了一段时间为大家挑选了一些页面,请根据最适合自己的进行阅读:【译者注:某些链接已无效,在此已删除】
[- 世界矩阵、视图矩阵和投影矩阵揭秘](http://web.archive.org/web/20131222170415/http:/robertokoci.com/world-view-projection-matrix-unveiled/)
[- 教程3:矩阵](http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/) 将为您简要介绍一下模型矩阵、视图矩阵与投影矩阵
[- 变换 (Direct3D 9)](https://msdn.microsoft.com/en-us/library/windows/desktop/bb206269(v=vs.85).aspx)
[- 3D简要介绍](http://inear.se/talk/a_brief_introduction_to_3d.pptx):一个优秀的PPT幻灯片!请至少读到27页,在此之后的内容也谈到OpenGL或DirectX链接到GPU的技术。
[- OpenGL变换](http://www.songho.ca/opengl/gl_transform.html)
![变换过程](https://box.kancloud.cn/2016-03-22_56f0e98872493.jpg "变换过程")
或许你已经知道了一些三角形的概念,这与API无关(OpenGL或DirectX),我们以后将看到。
通过阅读这些文章,你真正需要了解的是,有这样一连串的变换:
- 我们先围绕一个三维物体本身
- 对同一个对象移入虚拟的3D世界中通过矩阵操作进行平移、缩放或旋转
- 在3D世界中摄像机朝向这个三维物体
- 这个流程之后最终的结果将会投射在一个二维空间,也就是你的屏幕上
![坐标系](https://box.kancloud.cn/2016-03-22_56f0e98884b15.jpg "坐标系")
![坐标系](https://box.kancloud.cn/2016-03-22_56f0e9889669a.jpg "坐标系")
这一切都是通过矩阵神奇的运算累计变换完成的。在教程示例运行之前,你真的应该稍微了解这些概念。就算你不读这些文章就明白了这一切也应该去扫一眼,因为你日后在写3D软件渲染引擎时很可能会回去看。不过这是完全正常的,不用担心 !;) 因为最好的3D学习方式是通过试错。
我们不会花时间说矩阵是如何工作的,好消息是,你也不需要真正了解矩阵。我们简单的把它看成一个黑盒子,然后做正确的操作就行了。我不是矩阵的高手,但是我可以设法自己编写3D软件渲染引擎。所以,你这样做也可以取得成功。
然后,我们将使用为我们工作的库:
对于C#开发人员来说,我们可以用SharpDX,是一个DirectX的托管包装库。
对于JavaScript开发人员来说,我们可以使用由David Catuhe编写的Babylon.math.js库。
同时,我用TypeScript重写了Babylon.math.js库。
所需要的软件
我们可以编写C#语言开发的WinRT/XAML Windows Store Apps应用程序,或使用TypeScript/JavaScript开发的Html5应用程序。
那么,如果你想要使用C#进行开发,你需要安装:
1 - Windows 8及以上版本的操作系统
2 - Visual Studio Express for Windows Store Apps([点此下载](http://msdn.microsoft.com/en-US/windows/apps/br211386))或以上版本的Visual Studio IDE。
如果你选择使用TypeScript编写,你需要从这里[安装](http://www.typescriptlang.org/#Download)此语言。
你会发现这个插件是Visual Studio 2012版本的,但还有其他的选择:[Sublime Text, Vi, Emacs:TypeScript支持](http://msopentech.com/blog/2012/10/01/sublime-text-vi-emacs-typescript-enabled)!
【译者注:此处省略100个英文字母,给TypeScript打广告太明显,偏离了本章主题】
如果你选择JavaScript,你只需要安装你喜欢的IDE和Html5兼容的浏览器。:)
请使用你喜欢的语言创建一个名为“SoftEngine”的项目。如果选择的语言是C#,请使用NuGet添加“SharpDX core assembly”到你的解决方案中:
![SharpDX](https://box.kancloud.cn/2016-03-22_56f0e988a88a2.jpg "SharpDX")
如果是TypeScript,请下载[Babylon.math.ts](http://david.blob.core.windows.net/softengine3d/babylon.math.ts)。如果是JavaScrip,请下载[Babylon.math.js](http://david.blob.core.windows.net/softengine3d/babylon.math.js)。并进行引用。
**后台缓冲区 & 渲染循环**
一个3D引擎,我们绘制完整的场景以保持每秒60帧(FPS)为最佳。这样可以保证动画的流畅。
为了进行渲染工作,我们需要后台缓冲区。它可以被看作是一个映射屏幕大小区域的二维数组。数组中的每个元素都被映射为屏幕上的某一个像素。
在XAMLWindows Store Apps中,我们将使用一个byte[]数组,它将成为我们的动态后台缓冲区。
在我们的动画循环中(Tick),每一帧要进行渲染时,后台缓冲区都将影响到作为XAML图像源头的前台缓冲区的WriteableBitmap。
在渲染循环中,XAML渲染引擎调用我们时,帧就会被产生。注册事件代码为:
~~~
CompositionTarget.Rendering += CompositionTarget_Rendering;
~~~
在Html5中我们将使用 <canvas /> 元素。canvas元素已经具有一个后台缓冲区数组的关联。你可以通过getImageData()和setImageData()函数来访问它。
动画循环将有 [requestAnimationFrame()](http://msdn.microsoft.com/library/ie/hh920765(v=vs.85).aspx) 函数来处理。它是一个类似 setTimeout(function(){}, 1000 / 60); 的定时器实现。不同的是,只有当浏览器主动调用时才会执行,所以并不需要指定间隔时间。
**注意**:在”硬件缩放“的情况下,后台缓冲区和前台缓冲区的大小可以不同。使用”硬件缩放“可以得到更好的性能和更差的效果【译者注:我是故意把这里翻译成这样的,很喜感不是么……哈哈。】。更多关于这个话题的内容请看[这里](http://blogs.msdn.com/b/eternalcoding/archive/2012/03/22/unleash-the-power-of-html-5-canvas-for-gaming-part-1.aspx?Redirected=true)。
**摄像机 & 网格 对象**
让我们开始编码吧!首先,我们需要定义一些有关于摄像机和网格的细节。网格这个名称用来代指三维物体。
我们的摄像机有2个属性:它在3D世界的位置以及它所看向的方位。两者都是Vector3类型。在C#中用 SharpDX.Vector3,在TypeScript & JavaScript中使用BABYLON.Vector3。
我们的网格有顶点数组 (一些三维点),这将用来构建我们的三维物体,它在3D世界中的位置和它旋转的状态。
继续,我们需要下面的代码:
【译者注:C#代码】
~~~
// Camera.cs & Mesh.cs
using SharpDX;
namespace SoftEngine
{
public class Camera
{
public Vector3 Position { get; set; }
public Vector3 Target { get; set; }
}
public class Mesh
{
public string Name { get; set; }
public Vector3[] Vertices { get; private set; }
public Vector3 Position { get; set; }
public Vector3 Rotation { get; set; }
public Mesh(string name, int verticesCount)
{
Vertices = new Vector3[verticesCount];
Name = name;
}
}
}
~~~
【译者注:TypeScript代码】
~~~
//<reference path="babylon.math.ts"/>
module SoftEngine {
export class Camera {
Position: BABYLON.Vector3;
Target: BABYLON.Vector3;
constructor() {
this.Position = BABYLON.Vector3.Zero();
this.Target = BABYLON.Vector3.Zero();
}
}
export class Mesh {
Position: BABYLON.Vector3;
Rotation: BABYLON.Vector3;
Vertices: BABYLON.Vector3[];
constructor(public name: string, verticesCount: number) {
this.Vertices = new Array(verticesCount);
this.Rotation = BABYLON.Vector3.Zero();
this.Position = BABYLON.Vector3.Zero();
}
}
}
~~~
【译者注:JavaScript代码】
~~~
var SoftEngine;
(function (SoftEngine) {
var Camera = (function () {
function Camera() {
this.Position = BABYLON.Vector3.Zero();
this.Target = BABYLON.Vector3.Zero();
}
return Camera;
})();
SoftEngine.Camera = Camera;
var Mesh = (function () {
function Mesh(name, verticesCount) {
this.name = name;
this.Vertices = new Array(verticesCount);
this.Rotation = BABYLON.Vector3.Zero();
this.Position = BABYLON.Vector3.Zero();
}
return Mesh;
})();
SoftEngine.Mesh = Mesh;
})(SoftEngine || (SoftEngine = {}));
~~~
举例来说,如果你想使用我们的网格对象来描述一个立方体,你需要创建8个顶点(vetices)来关联到8个点(points)。下面是在Blender中显示的立方体坐标。
![立方体坐标点](https://box.kancloud.cn/2016-03-22_56f0e988c04d1.jpg "立方体坐标点")
用的是左手坐标系。请别忘了,当你创建一个网格,坐标系开始点再网格的中心。因此,X=0, Y=0, Z=0是立方体的中心点。
这个立方体可以通过这样的代码来创建:
~~~
var mesh = new Mesh("Cube", 8);
mesh.Vertices[0] = new Vector3(-1, 1, 1);
mesh.Vertices[1] = new Vector3(1, 1, 1);
mesh.Vertices[2] = new Vector3(-1, -1, 1);
mesh.Vertices[3] = new Vector3(-1, -1, -1);
mesh.Vertices[4] = new Vector3(-1, 1, -1);
mesh.Vertices[5] = new Vector3(1, 1, -1);
mesh.Vertices[6] = new Vector3(1, -1, 1);
mesh.Vertices[7] = new Vector3(1, -1, -1);
~~~
**最重要的部分:设备对象**
现在,我们有了基本对象,我们知道如何构建一个三维网格。但是我们还缺少一个最重要的部分:设备对象。这是我们的3D引擎核心部分。
在引擎渲染的函数内,我们将建立投影矩阵,并根据我们预先定义过摄像机获得视图矩阵。然后我们遍历每个网格提供基于目前的旋转和平移值来构建世界矩阵。
最后,得到这三个矩阵后,我们就可以这样得到最终的变换矩阵:
~~~
var transformMatrix = worldMatrix * viewMatrix * projectionMatrix;
~~~
你绝对需要通过阅读前面的“阅读前提条件”来理解这个概念。否则,你可能会简单的Copy/Paste代码,而无须了解有关神奇的实现。这对于之后的学习并没有太大的影响,但是理解它你能更好的进行编码。
使用该变换矩阵,我们将项目中每个网格的每个顶点从X, Y, Z坐标转换到2D世界中的X, Y坐标,最终在屏幕上绘制。
我们增加一小段逻辑,只通过 PutPixel(方法/函数)进行绘制显示。
这里有各种版本的设备对象。我增加了些注释,以帮助您更多的理解它。【译者注:还不是要一个一个翻译!】
**注**:微软Windows使用BGRA颜色空间(蓝色,绿色,红色,阿尔法),而Html5的画布使用的是[RGBA](http://en.wikipedia.org/wiki/RGBA_color_space)颜色空间(红,绿,蓝,阿尔法)。
这就是你就发现为什么C#和Html5代码有一些小差别的原因。
【译者注:C#代码】
~~~
using Windows.UI.Xaml.Media.Imaging;
using System.Runtime.InteropServices.WindowsRuntime;
using SharpDX;
namespace SoftEngine
{
public class Device
{
private byte[] backBuffer;
private WriteableBitmap bmp;
public Device(WriteableBitmap bmp)
{
this.bmp = bmp;
// 后台缓冲区大小值是要绘制的像素
// 屏幕(width*height) * 4 (R,G,B & Alpha值)
backBuffer = new byte[bmp.PixelWidth * bmp.PixelHeight * 4];
}
// 清除后台缓冲区为指定颜色
public void Clear(byte r, byte g, byte b, byte a)
{
for (var index = 0; index < backBuffer.Length; index += 4)
{
// Windows使用BGRA,而不是Html5中使用的RGBA
backBuffer[index] = b;
backBuffer[index + 1] = g;
backBuffer[index + 2] = r;
backBuffer[index + 3] = a;
}
}
// 当一切准备就绪时,我们就可以
// 刷新后台缓冲区到前台缓冲区
public void Present()
{
using (var stream = bmp.PixelBuffer.AsStream())
{
// 将我们的byte[]后台缓冲区写入到WriteableBitmap流
stream.Write(backBuffer, 0, backBuffer.Length);
}
// 请求将整个位图重绘
bmp.Invalidate();
}
// 调用此方法把一个像素绘制到指定的X, Y坐标上
public void PutPixel(int x, int y, Color4 color)
{
// 我们的后台缓冲区是一维数组
// 这里我们简单计算,将X和Y对应到此一维数组中
var index = (x + y * bmp.PixelWidth) * 4;
backBuffer[index] = (byte)(color.Blue * 255);
backBuffer[index + 1] = (byte)(color.Green * 255);
backBuffer[index + 2] = (byte)(color.Red * 255);
backBuffer[index + 3] = (byte)(color.Alpha * 255);
}
// 将三维坐标和变换矩阵转换成二维坐标
public Vector2 Project(Vector3 coord, Matrix transMat)
{
// 进行坐标变换
var point = Vector3.TransformCoordinate(coord, transMat);
// 变换后的坐标起始点是坐标系的中心点
// 但是,在屏幕上,我们以左上角为起始点
// 我们需要重新计算使他们的起始点变成左上角
var x = point.X * bmp.PixelWidth + bmp.PixelWidth / 2.0f;
var y = -point.Y * bmp.PixelHeight + bmp.PixelHeight / 2.0f;
return (new Vector2(x, y));
}
// 如果二维坐标在可视范围内则绘制
public void DrawPoint(Vector2 point)
{
// 判断是否在屏幕内
if (point.X >= 0 && point.Y >= 0 && point.X < bmp.PixelWidth && point.Y < bmp.PixelHeight)
{
// 绘制一个黄色点
PutPixel((int)point.X, (int)point.Y, new Color4(1.0f, 1.0f, 0.0f, 1.0f));
}
}
// 主循环体,每一帧,引擎都要计算顶点投射
public void Render(Camera camera, params Mesh[] meshes)
{
// 要理解这个部分,请阅读“阅读前提条件”
var viewMatrix = Matrix.LookAtLH(camera.Position, camera.Target, Vector3.UnitY);
var projectionMatrix = Matrix.PerspectiveFovRH(0.78f,
(float)bmp.PixelWidth / bmp.PixelHeight,
0.01f, 1.0f);
foreach (Mesh mesh in meshes)
{
// 请注意,在平移前要先旋转
var worldMatrix = Matrix.RotationYawPitchRoll(mesh.Rotation.Y,
mesh.Rotation.X, mesh.Rotation.Z) *
Matrix.Translation(mesh.Position);
var transformMatrix = worldMatrix * viewMatrix * projectionMatrix;
foreach (var vertex in mesh.Vertices)
{
// 首先,我们将三维空间转换为二维空间
var point = Project(vertex, transformMatrix);
// 然后我们就可以在屏幕画出点
DrawPoint(point);
}
}
}
}
}
~~~
【译者注: TypeScript代码】
~~~
///<reference path="babylon.math.ts"/>
module SoftEngine {
export class Device {
// 后台缓冲区大小值是要绘制的像素
// 屏幕(width*height) * 4 (R,G,B & Alpha值)
private backbuffer: ImageData;
private workingCanvas: HTMLCanvasElement;
private workingContext: CanvasRenderingContext2D;
private workingWidth: number;
private workingHeight: number;
// 等于backbuffer.data
private backbufferdata;
constructor(canvas: HTMLCanvasElement) {
this.workingCanvas = canvas;
this.workingWidth = canvas.width;
this.workingHeight = canvas.height;
this.workingContext = this.workingCanvas.getContext("2d");
}
// 清除后台缓冲区为指定颜色
public clear(): void {
// 默认清除为黑色
this.workingContext.clearRect(0, 0, this.workingWidth, this.workingHeight);
// 一旦用黑色像素清除我们要找回相关图像数据,以清楚后台缓冲区
this.backbuffer = this.workingContext.getImageData(0, 0, this.workingWidth, this.workingHeight);
}
// 当一切就绪后将后台缓冲区刷新到前台缓冲区
public present(): void {
this.workingContext.putImageData(this.backbuffer, 0, 0);
}
// 调用此方法把一个像素绘制到指定的X, Y坐标上
public putPixel(x: number, y: number, color: BABYLON.Color4): void {
this.backbufferdata = this.backbuffer.data;
// 我们的后台缓冲区是一维数组
// 这里我们简单计算,将X和Y对应到此一维数组中
var index: number = ((x >> 0) + (y >> 0) * this.workingWidth) * 4;
// 在Html5 canvas中使用RGBA颜色空间
this.backbufferdata[index] = color.r * 255;
this.backbufferdata[index + 1] = color.g * 255;
this.backbufferdata[index + 2] = color.b * 255;
this.backbufferdata[index + 3] = color.a * 255;
}
// 将三维坐标和变换矩阵转换成二维坐标
public project(coord: BABYLON.Vector3, transMat: BABYLON.Matrix): BABYLON.Vector2 {
// 进行坐标变换
var point = BABYLON.Vector3.TransformCoordinates(coord, transMat);
// 变换后的坐标起始点是坐标系的中心点
// 但是,在屏幕上,我们以左上角为起始点
// 我们需要重新计算使他们的起始点变成左上角
var x = point.x * this.workingWidth + this.workingWidth / 2.0 >> 0;
var y = -point.y * this.workingHeight + this.workingHeight / 2.0 >> 0;
return (new BABYLON.Vector2(x, y));
}
// 如果二维坐标在可视范围内则绘制
public drawPoint(point: BABYLON.Vector2): void {
// 判断是否在屏幕内
if (point.x >= 0 && point.y >= 0 && point.x < this.workingWidth
&& point.y < this.workingHeight) {
// 绘制一个黄色点
this.putPixel(point.x, point.y, new BABYLON.Color4(1, 1, 0, 1));
}
}
// 主循环体,每一帧,引擎都要计算顶点投射
public render(camera: Camera, meshes: Mesh[]): void {
// 要理解这个部分,请阅读“阅读前提条件”
var viewMatrix = BABYLON.Matrix.LookAtLH(camera.Position, camera.Target, BABYLON.Vector3.Up());
var projectionMatrix = BABYLON.Matrix.PerspectiveFovLH(0.78,
this.workingWidth / this.workingHeight, 0.01, 1.0);
for (var index = 0; index < meshes.length; index++) {
// 缓存当前网格对象
var cMesh = meshes[index];
// 请注意,在平移前要先旋转
var worldMatrix = BABYLON.Matrix.RotationYawPitchRoll(
cMesh.Rotation.y, cMesh.Rotation.x, cMesh.Rotation.z)
.multiply(BABYLON.Matrix.Translation(
cMesh.Position.x, cMesh.Position.y, cMesh.Position.z));
var transformMatrix = worldMatrix.multiply(viewMatrix).multiply(projectionMatrix);
for (var indexVertices = 0; indexVertices < cMesh.Vertices.length; indexVertices++) {
// 首先,我们将三维空间转换为二维空间
var projectedPoint = this.project(cMesh.Vertices[indexVertices], transformMatrix);
// 然后我们就可以在屏幕画出点
this.drawPoint(projectedPoint);
}
}
}
}
}
~~~
【译者注:JavaScript代码】
~~~
var SoftEngine;
(function (SoftEngine) {
var Device = (function () {
function Device(canvas) {
// 后台缓冲区大小值是要绘制的像素
// 屏幕(width*height) * 4 (R,G,B & Alpha值)
this.workingCanvas = canvas;
this.workingWidth = canvas.width;
this.workingHeight = canvas.height;
this.workingContext = this.workingCanvas.getContext("2d");
}
// 清除后台缓冲区为指定颜色
Device.prototype.clear = function () {
// 默认清除为黑色
this.workingContext.clearRect(0, 0, this.workingWidth, this.workingHeight);
// 一旦用黑色像素清除我们要找回相关图像数据,以清楚后台缓冲区
this.backbuffer = this.workingContext.getImageData(0, 0, this.workingWidth, this.workingHeight);
};
// 当一切就绪后将后台缓冲区刷新到前台缓冲区
Device.prototype.present = function () {
this.workingContext.putImageData(this.backbuffer, 0, 0);
};
// 调用此方法把一个像素绘制到指定的X, Y坐标上
Device.prototype.putPixel = function (x, y, color) {
this.backbufferdata = this.backbuffer.data;
// 我们的后台缓冲区是一维数组
// 这里我们简单计算,将X和Y对应到此一维数组中
var index = ((x >> 0) + (y >> 0) * this.workingWidth) * 4;
// 在Html5 canvas中使用RGBA颜色空间
this.backbufferdata[index] = color.r * 255;
this.backbufferdata[index + 1] = color.g * 255;
this.backbufferdata[index + 2] = color.b * 255;
this.backbufferdata[index + 3] = color.a * 255;
};
// 将三维坐标和变换矩阵转换成二维坐标
Device.prototype.project = function (coord, transMat) {
// 进行坐标变换
var point = BABYLON.Vector3.TransformCoordinates(coord, transMat);
// 变换后的坐标起始点是坐标系的中心点
// 但是,在屏幕上,我们以左上角为起始点
// 我们需要重新计算使他们的起始点变成左上角
var x = point.x * this.workingWidth + this.workingWidth / 2.0 >> 0;
var y = -point.y * this.workingHeight + this.workingHeight / 2.0 >> 0;
return (new BABYLON.Vector2(x, y));
};
// 如果二维坐标在可视范围内则绘制
Device.prototype.drawPoint = function (point) {
// 判断是否在屏幕内
if (point.x >= 0 && point.y >= 0 && point.x < this.workingWidth
&& point.y < this.workingHeight) {
// 绘制一个黄色点
this.putPixel(point.x, point.y, new BABYLON.Color4(1, 1, 0, 1));
}
};
// 主循环体,每一帧,引擎都要计算顶点投射
Device.prototype.render = function (camera, meshes) {
// 要理解这个部分,请阅读“阅读前提条件”
var viewMatrix = BABYLON.Matrix.LookAtLH(camera.Position, camera.Target, BABYLON.Vector3.Up());
var projectionMatrix = BABYLON.Matrix.PerspectiveFovLH(0.78,
this.workingWidth / this.workingHeight, 0.01, 1.0);
for (var index = 0; index < meshes.length; index++) {
// 缓存当前网格对象
var cMesh = meshes[index];
// 请注意,在平移前要先旋转
var worldMatrix = BABYLON.Matrix.RotationYawPitchRoll(
cMesh.Rotation.y, cMesh.Rotation.x, cMesh.Rotation.z)
.multiply(BABYLON.Matrix.Translation(
cMesh.Position.x, cMesh.Position.y, cMesh.Position.z));
var transformMatrix = worldMatrix.multiply(viewMatrix).multiply(projectionMatrix);
for (var indexVertices = 0; indexVertices < cMesh.Vertices.length; indexVertices++) {
// 首先,我们将三维空间转换为二维空间
var projectedPoint = this.project(cMesh.Vertices[indexVertices], transformMatrix);
// 然后我们就可以在屏幕画出点
this.drawPoint(projectedPoint);
}
}
};
return Device;
})();
SoftEngine.Device = Device;
})(SoftEngine || (SoftEngine = {}));
~~~
**结合到一起**
最后我们需要建立一个网格(我们的立方体),创建一个摄像机,并面向我们的网格,然后实例化设备对象。
一旦这样做,我们将运行动画/渲染循环。在理想的情况下,这个循环将每隔16毫秒(60FPS)执行一次。在每个循环周期,做了这样几件事:
1 - 清空屏幕并且将所有像素变黑(*Clear()* function)。
2 - 对网格更新位置和旋转值。
3 - 计算矩阵,渲染到后台缓冲区( * Render()* function)。
4 - 将后台缓冲区数据刷新到前台缓冲区以显示( * Present()* function)。
【译者注:C#代码】
~~~
private Device device;
Mesh mesh = new Mesh("Cube", 8);
Camera mera = new Camera();
private void Page_Loaded(object sender, RoutedEventArgs e)
{
// 在这里设置后台缓冲区的分辨率
WriteableBitmap bmp = new WriteableBitmap(640, 480);
device = new Device(bmp);
// 设置我们的XAML图像源
frontBuffer.Source = bmp;
mesh.Vertices[0] = new Vector3(-1, 1, 1);
mesh.Vertices[1] = new Vector3(1, 1, 1);
mesh.Vertices[2] = new Vector3(-1, -1, 1);
mesh.Vertices[3] = new Vector3(-1, -1, -1);
mesh.Vertices[4] = new Vector3(-1, 1, -1);
mesh.Vertices[5] = new Vector3(1, 1, -1);
mesh.Vertices[6] = new Vector3(1, -1, 1);
mesh.Vertices[7] = new Vector3(1, -1, -1);
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);
// 每一帧都稍微转动一下立方体
mesh.Rotation = new Vector3(mesh.Rotation.X + 0.01f, mesh.Rotation.Y + 0.01f, mesh.Rotation.Z);
// 做各种矩阵运算
device.Render(mera, mesh);
// 刷新后台缓冲区到前台缓冲区
device.Present();
}
~~~
【译者注:TypeScript代码】
~~~
///<reference path="SoftEngine.ts"/>
var canvas: HTMLCanvasElement;
var device: SoftEngine.Device;
var mesh: SoftEngine.Mesh;
var meshes: SoftEngine.Mesh[] = [];
var mera: SoftEngine.Camera;
document.addEventListener("DOMContentLoaded", init, false);
function init()
{
canvas = < HTMLCanvasElement > document.getElementById("frontBuffer");
mesh = new SoftEngine.Mesh("Cube", 8);
meshes.push(mesh);
mera = new SoftEngine.Camera();
device = new SoftEngine.Device(canvas);
mesh.Vertices[0] = new BABYLON.Vector3(-1, 1, 1);
mesh.Vertices[1] = new BABYLON.Vector3(1, 1, 1);
mesh.Vertices[2] = new BABYLON.Vector3(-1, -1, 1);
mesh.Vertices[3] = new BABYLON.Vector3(-1, -1, -1);
mesh.Vertices[4] = new BABYLON.Vector3(-1, 1, -1);
mesh.Vertices[5] = new BABYLON.Vector3(1, 1, -1);
mesh.Vertices[6] = new BABYLON.Vector3(1, -1, 1);
mesh.Vertices[7] = new BABYLON.Vector3(1, -1, -1);
mera.Position = new BABYLON.Vector3(0, 0, 10);
mera.Target = new BABYLON.Vector3(0, 0, 0);
// 调用Html5渲染循环
requestAnimationFrame(drawingLoop);
}
// 渲染循环处理
function drawingLoop()
{
device.clear();
// 每帧都稍微转动一下立方体
mesh.Rotation.x += 0.01;
mesh.Rotation.y += 0.01;
// 做各种矩阵运算
device.render(mera, meshes);
// 刷新后台缓冲区到前台缓冲区
device.present();
// 递归调用Html5渲染循环
requestAnimationFrame(drawingLoop);
}
~~~
【译者注:JavaScript代码】
~~~
var canvas;
var device;
var mesh;
var meshes = [];
var mera;
document.addEventListener("DOMContentLoaded", init, false);
function init() {
canvas = document.getElementById("frontBuffer");
mesh = new SoftEngine.Mesh("Cube", 8);
meshes.push(mesh);
mera = new SoftEngine.Camera();
device = new SoftEngine.Device(canvas);
mesh.Vertices[0] = new BABYLON.Vector3(-1, 1, 1);
mesh.Vertices[1] = new BABYLON.Vector3(1, 1, 1);
mesh.Vertices[2] = new BABYLON.Vector3(-1, -1, 1);
mesh.Vertices[3] = new BABYLON.Vector3(-1, -1, -1);
mesh.Vertices[4] = new BABYLON.Vector3(-1, 1, -1);
mesh.Vertices[5] = new BABYLON.Vector3(1, 1, -1);
mesh.Vertices[6] = new BABYLON.Vector3(1, -1, 1);
mesh.Vertices[7] = new BABYLON.Vector3(1, -1, -1);
mera.Position = new BABYLON.Vector3(0, 0, 10);
mera.Target = new BABYLON.Vector3(0, 0, 0);
// 调用Html5渲染循环
requestAnimationFrame(drawingLoop);
}
// 渲染循环处理
function drawingLoop() {
device.clear();
// 每帧都稍微转动一下立方体
mesh.Rotation.x += 0.01;
mesh.Rotation.y += 0.01;
// 做各种矩阵运算
device.render(mera, meshes);
// 刷新后台缓冲区到前台缓冲区
device.present();
// 递归调用Html5渲染循环
requestAnimationFrame(drawingLoop);
}
~~~
如果你已经正确的遵循这第一个教程的话,你应该已经得到这样的效果:
[点我运行](http://david.blob.core.windows.net/softengine3d/part1/index.html)
如果没有,下载源代码:
C#:[SoftEngineCSharpPart1.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineCSharpPart1.zip)
TypeScript:[SoftEngineTSPart1.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineTSPart1.zip)
JavaScript:[SoftEngineJSPart1.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineJSPart1.zip) 或只需右键点击 -> 查看框架的源代码
简单的检查代码并试图找到是什么地方除了差错。 :)
下一章节,我们将学习面和三角形的概念来绘制每个顶点之间的线段。
![下一章节预览](https://box.kancloud.cn/2016-03-22_56f0e988d78ad.jpg "下一章节预览")