譯者前言:
本文譯自[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中編寫3D軟件渲染引擎中的從Blender加載導出網格這一章節。
我們已經能夠在引擎中加載從Blender導出的Json文件了。那么到現在為止,我們的渲染效果依然只是簡單的線框渲染。但是,在本章我們將講解如何使用三角形光柵化算法來填充三角形。然后,我們將使用深度緩沖,以避免在后面的面跑到前面來的問題。
本章教程是以下系列的一部分:
[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 –填充光柵化的三角形并使用深度緩沖(本文)
[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/part4/index.html)
**光柵化**
【譯者注:感謝四川-平生小哥為我們翻譯此段】
有很多不同類型的光柵化算法。我甚至知道在我的團隊中有人向知名的GPU廠商提出了自己的光柵化算法。也多虧了他,我現在知道了什么是[折行書](https://en.wikipedia.org/wiki/Boustrophedon)并一直使用至今。 :-)
為了更正規,我們將在本教程中實現一個簡單而有效的光柵化算法。正如我們在CPU中運行3D軟件渲染引擎一般,它會耗費我們的大量CPU運算。當然,現今這個功能已經直接用GPU來幫我們完成。
先讓我們做一個練習。請拿出一張紙,然后畫一個三角形,嗯……任意你所能畫出來的三角形。我們要找出一個通用的方法可以得出任意類型的三角形。
如果我們按Y軸對每個三角形的三個點進行排序,保證 P1 后面是 P2, 然后是 P3 的話,最終將出現兩種可能的情況:

你將會看到這兩種情況:
P2 在 P1 和 P3 的右側 或 P2在 P1 和 P3 的左側。在本教程中,由于我們始終是從左到右(sx 到 se)的順序畫線,所以就按照這個假設來處理這兩種情況。
此外,我們要順著左圖中的紅線自上而下 (從 P1.Y 到 ?P3.Y) 從左向右繪制。但是當到達P2.Y時我們需要稍微改變一下邏輯,因為這時兩種情況的斜率都會發生改變。這就是為什么我們將掃描線處理分為兩個步驟:從 P1.Y 向下移動到 P2.Y,然后從P2.Y 最終移動到 P3.Y。
要了解我們使用算法的全部邏輯,可以在維基百科中找到詞條:[http://en.wikipedia.org/wiki/Slope](http://en.wikipedia.org/wiki/Slope "http://en.wikipedia.org/wiki/Slope")。它只是一些基本的數學運算。
為了能夠適應這兩種情況,你只需要進行簡單的運算:
dP1P2 = P2.X - P1.X / P2.Y - P1.Y
dP1P3 = P3.X - P1.X / P3.Y - P1.Y
那我們如何得知是屬于哪種情況呢?
P2 在右的第一種情況:dP1P2 > dP1P3
P2 在左的第二種情況:dP1P3 > dP1P2?
現在已經有了算法的基本邏輯,我們需要知道如何計算上圖中每條線上的 sx(起始的 x 坐標) 和 ex(結束的 x 坐標) 之間的 x。因此要首先計算出 sx 和 ex。由于我們知道當前所掃描到的 y 值、P1P3 和 P1P3 的斜率,因此我們不難得出 sx 和 ex 值。
以情況1為例。首先利用當前的 y 值來計算梯度。它將告訴我們在 P1.Y 和 P2.Y之間進行處理時,我們當前所處的階段。
梯度 = 當前的y值 - P1.Y / P2.Y - P1.Y
因為 x 和 y 是線性連接,所以我們可以基于該梯度,利用 P1.X 和 P3.X 來計算 sx 插值,并利用 P1.X 和 P2.X來計算 ex 插值。
如果您能夠理解插值這個概念,那么你就能夠理解剩下所有關于光纖和材質。而且你能夠很好的閱讀相關代碼,也能夠從頭開始自己重寫代碼,而無須復制、粘貼下面的代碼。
如果還不是很清楚的話,這里有一些關于光柵化的文章以供閱讀:
[- 3D軟件渲染引擎 - 第一部分](http://www.codeproject.com/Articles/170296/3D-Software-Rendering-Engine-Part-I)
[- 三角形光柵化](https://lva.cg.tuwien.ac.at/ecg/wiki/doku.php?id=students:fill_rasterization)
[- 填充三角形的軟件光柵化算法](http://www.sunshine2k.de/coding/java/TriangleRasterization/TriangleRasterization.html)
現在,基于我們現有的算法描述說明讓我們開始編寫代碼。首先,從設備對象刪除 drawLine 和 drawBline 函數,并用下面的代碼進行替換:
【譯者注:C#代碼】
~~~
// 將三維坐標和變換矩陣轉換成二維坐標
public Vector3 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 Vector3(x, y, point.Z));
}
// 如果二維坐標在可視范圍內則繪制
public void DrawPoint(Vector2 point, Color4 color)
{
// 判斷是否在屏幕內
if (point.X >= 0 && point.Y >= 0 && point.X < bmp.PixelWidth && point.Y < bmp.PixelHeight)
{
// 繪制一個點
PutPixel((int)point.X, (int)point.Y, color);
}
}
~~~
【譯者注:TypeScript代碼】
~~~
// 將三維坐標和變換矩陣轉換成二維坐標
public project(coord: BABYLON.Vector3, transMat: BABYLON.Matrix): BABYLON.Vector3 {
// 進行坐標變換
var point = BABYLON.Vector3.TransformCoordinates(coord, transMat);
// 變換后的坐標起始點是坐標系的中心點
// 但是,在屏幕上,我們以左上角為起始點
// 我們需要重新計算使他們的起始點變成左上角
var x = point.x * this.workingWidth + this.workingWidth / 2.0;
var y = -point.y * this.workingHeight + this.workingHeight / 2.0;
return (new BABYLON.Vector3(x, y, point.z));
}
// 如果二維坐標在可視范圍內則繪制
public drawPoint(point: BABYLON.Vector2, color: BABYLON.Color4): void {
// 判斷是否在屏幕內
if(point.x >= 0 && point.y >= 0 && point.x < this.workingWidth && point.y < this.workingHeight) {
// 繪制一個點
this.putPixel(point.x, point.y, color);
}
}
~~~
【譯者注:JavaScript代碼】
~~~
// 將三維坐標和變換矩陣轉換成二維坐標
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.Vector3(x, y, point.z));
};
// 如果二維坐標在可視范圍內則繪制
Device.prototype.drawPoint = function (point, color) {
// 判斷是否在屏幕內
if (point.x >= 0 && point.y >= 0 && point.x < this.workingWidth
&& point.y < this.workingHeight) {
// 繪制一個點
this.putPixel(point.x, point.y, color);
}
};
~~~
我們僅僅做了一些準備,下面則是最重要的部分,基于先前解釋過的三角形的邏輯進行繪制。
【譯者注:C#代碼】
~~~
// 限制數值范圍在0和1之間
float Clamp(float value, float min = 0, float max = 1)
{
return Math.Max(min, Math.Min(value, max));
}
// 過渡插值
float Interpolate(float min, float max, float gradient)
{
return min + (max - min) * Clamp(gradient);
}
// 在兩點之間從左到右繪制一條線段
// papb -> pcpd
// pa, pb, pc, pd在之前必須已經排好序
void ProcessScanLine(int y, Vector3 pa, Vector3 pb, Vector3 pc, Vector3 pd, Color4 color)
{
// 由當前的y值,我們可以計算出梯度
// 以此再計算出 起始X(sx) 和 結束X(ex)
// 如果pa.Y == pb.Y 或者 pc.Y== pd.y的話,梯度強制為1
var gradient1 = pa.Y != pb.Y ? (y - pa.Y) / (pb.Y - pa.Y) : 1;
var gradient2 = pc.Y != pd.Y ? (y - 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);
// 從左(sx)向右(ex)繪制一條線
for (var x = sx; x < ex; x++)
{
DrawPoint(new Vector2(x, y), color);
}
}
public void DrawTriangle(Vector3 p1, Vector3 p2, Vector3 p3, Color4 color)
{
// 進行排序,p1總在最上面,p2總在最中間,p3總在最下面
if (p1.Y > p2.Y)
{
var temp = p2;
p2 = p1;
p1 = temp;
}
if (p2.Y > p3.Y)
{
var temp = p2;
p2 = p3;
p3 = temp;
}
if (p1.Y > p2.Y)
{
var temp = p2;
p2 = p1;
p1 = temp;
}
// 反向斜率
float dP1P2, dP1P3;
// http://en.wikipedia.org/wiki/Slope
// 計算反向斜率
if (p2.Y - p1.Y > 0)
dP1P2 = (p2.X - p1.X) / (p2.Y - p1.Y);
else
dP1P2 = 0;
if (p3.Y - p1.Y > 0)
dP1P3 = (p3.X - p1.X) / (p3.Y - p1.Y);
else
dP1P3 = 0;
// 對于第一種情況來說,三角形是這樣的:
// P1
// -
// --
// - -
// - -
// - - P2
// - -
// - -
// -
// P3
if (dP1P2 > dP1P3)
{
for (var y = (int)p1.Y; y <= (int)p3.Y; y++)
{
if (y < p2.Y)
{
ProcessScanLine(y, p1, p3, p1, p2, color);
}
else
{
ProcessScanLine(y, p1, p3, p2, p3, color);
}
}
}
// 對于第二種情況來說,三角形是這樣的:
// P1
// -
// --
// - -
// - -
// P2 - -
// - -
// - -
// -
// P3
else
{
for (var y = (int)p1.Y; y <= (int)p3.Y; y++)
{
if (y < p2.Y)
{
ProcessScanLine(y, p1, p2, p1, p3, color);
}
else
{
ProcessScanLine(y, p2, p3, p1, p3, color);
}
}
}
}
~~~
【譯者注:TypeScript代碼】
~~~
// 限制數值范圍在0和1之間
public clamp(value: number, min: number = 0, max: number = 1): number {
return Math.max(min, Math.min(value, max));
}
// 過渡插值
public interpolate(min: number, max: number, gradient: number) {
return min + (max - min) * this.clamp(gradient);
}
// 在兩點之間從左到右繪制一條線段
// papb -> pcpd
// pa, pb, pc, pd在之前必須已經排好序
public processScanLine(y: number, pa: BABYLON.Vector3, pb: BABYLON.Vector3,
pc: BABYLON.Vector3, pd: BABYLON.Vector3, color: BABYLON.Color4): void {
// 由當前的y值,我們可以計算出梯度
// 以此再計算出 起始X(sx) 和 結束X(ex)
// 如果pa.Y == pb.Y 或者 pc.Y== pd.y的話,梯度強制為1
var gradient1 = pa.y != pb.y ? (y - pa.y) / (pb.y - pa.y) : 1;
var gradient2 = pc.y != pd.y ? (y - 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;
// 從左(sx)向右(ex)繪制一條線
for (var x = sx; x < ex; x++) {
this.drawPoint(new BABYLON.Vector2(x, y), color);
}
}
public drawTriangle(p1: BABYLON.Vector3, p2: BABYLON.Vector3,
p3: BABYLON.Vector3, color: BABYLON.Color4): void {
// 進行排序,p1總在最上面,p2總在最中間,p3總在最下面
if (p1.y > p2.y) {
var temp = p2;
p2 = p1;
p1 = temp;
}
if (p2.y > p3.y) {
var temp = p2;
p2 = p3;
p3 = temp;
}
if (p1.y > p2.y) {
var temp = p2;
p2 = p1;
p1 = temp;
}
// 反向斜率
var dP1P2: number; var dP1P3: number;
// http://en.wikipedia.org/wiki/Slope
// 計算反向斜率
if (p2.y - p1.y > 0)
dP1P2 = (p2.x - p1.x) / (p2.y - p1.y);
else
dP1P2 = 0;
if (p3.y - p1.y > 0)
dP1P3 = (p3.x - p1.x) / (p3.y - p1.y);
else
dP1P3 = 0;
// 對于第一種情況來說,三角形是這樣的:
// P1
// -
// --
// - -
// - -
// - - P2
// - -
// - -
// -
// P3
if (dP1P2 > dP1P3) {
for (var y = p1.y >> 0; y <= p3.y >> 0; y++)
{
if (y < p2.y) {
this.processScanLine(y, p1, p3, p1, p2, color);
}
else {
this.processScanLine(y, p1, p3, p2, p3, color);
}
}
}
// 對于第二種情況來說,三角形是這樣的:
// P1
// -
// --
// - -
// - -
// P2 - -
// - -
// - -
// -
// P3
else {
for (var y = p1.y >> 0; y <= p3.y >> 0; y++)
{
if (y < p2.y) {
this.processScanLine(y, p1, p2, p1, p3, color);
}
else {
this.processScanLine(y, p2, p3, p1, p3, color);
}
}
}
}
~~~
【譯者注:JavaScript代碼】
~~~
// 限制數值范圍在0和1之間
Device.prototype.clamp = function (value, min, max) {
if (typeof min === "undefined") { min = 0; }
if (typeof max === "undefined") { max = 1; }
return Math.max(min, Math.min(value, max));
};
// 過渡插值
Device.prototype.interpolate = function (min, max, gradient) {
return min + (max - min) * this.clamp(gradient);
};
// 在兩點之間從左到右繪制一條線段
// papb -> pcpd
// pa, pb, pc, pd在之前必須已經排好序
Device.prototype.processScanLine = function (y, pa, pb, pc, pd, color) {
// 由當前的y值,我們可以計算出梯度
// 以此再計算出 起始X(sx) 和 結束X(ex)
// 如果pa.Y == pb.Y 或者 pc.Y== pd.y的話,梯度強制為1
var gradient1 = pa.y != pb.y ? (y - pa.y) / (pb.y - pa.y) : 1;
var gradient2 = pc.y != pd.y ? (y - 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;
// 從左(sx)向右(ex)繪制一條線
for (var x = sx; x < ex; x++) {
this.drawPoint(new BABYLON.Vector2(x, y), color);
}
};
Device.prototype.drawTriangle = function (p1, p2, p3, color) {
// 進行排序,p1總在最上面,p2總在最中間,p3總在最下面
if (p1.y > p2.y) {
var temp = p2;
p2 = p1;
p1 = temp;
}
if (p2.y > p3.y) {
var temp = p2;
p2 = p3;
p3 = temp;
}
if (p1.y > p2.y) {
var temp = p2;
p2 = p1;
p1 = temp;
}
// 反向斜率
var dP1P2; var dP1P3;
// http://en.wikipedia.org/wiki/Slope
// 計算反向斜率
if (p2.y - p1.y > 0) {
dP1P2 = (p2.x - p1.x) / (p2.y - p1.y);
} else {
dP1P2 = 0;
}
if (p3.y - p1.y > 0) {
dP1P3 = (p3.x - p1.x) / (p3.y - p1.y);
} else {
dP1P3 = 0;
}
// 對于第一種情況來說,三角形是這樣的:
// P1
// -
// --
// - -
// - -
// - - P2
// - -
// - -
// -
// P3
if (dP1P2 > dP1P3) {
for (var y = p1.y >> 0; y <= p3.y >> 0; y++) {
if (y < p2.y) {
this.processScanLine(y, p1, p3, p1, p2, color);
} else {
this.processScanLine(y, p1, p3, p2, p3, color);
}
}
}
// 對于第二種情況來說,三角形是這樣的:
// P1
// -
// --
// - -
// - -
// P2 - -
// - -
// - -
// -
// P3
else {
for (var y = p1.y >> 0; y <= p3.y >> 0; y++) {
if (y < p2.y) {
this.processScanLine(y, p1, p2, p1, p3, color);
} else {
this.processScanLine(y, p2, p3, p1, p3, color);
}
}
}
};
~~~
你已經了解了如何處理兩種三角形的填寫以及掃描線中所做的操作了。
最后,你需要更新渲染函數,用drawTriangle來替代drawLine和drawBline。我們還用了不同的灰色填充每個三角形。不然的話,整個畫面一片灰你根本就看不出效果來。我們將在接下來的教程中學習到如何恰當的處理光照。
【譯者注:C#代碼】
~~~
var faceIndex = 0;
foreach (var face in mesh.Faces)
{
var vertexA = mesh.Vertices[face.A];
var vertexB = mesh.Vertices[face.B];
var vertexC = mesh.Vertices[face.C];
var pixelA = Project(vertexA, transformMatrix);
var pixelB = Project(vertexB, transformMatrix);
var pixelC = Project(vertexC, transformMatrix);
var color = 0.25f + (faceIndex % mesh.Faces.Length) * 0.75f / mesh.Faces.Length;
DrawTriangle(pixelA, pixelB, pixelC, new Color4(color, color, color, 1));
faceIndex++;
}
~~~
【譯者注:TypeScript代碼】
~~~
for (var indexFaces = 0; indexFaces < cMesh.Faces.length; indexFaces++) {
var currentFace = cMesh.Faces[indexFaces];
var vertexA = cMesh.Vertices[currentFace.A];
var vertexB = cMesh.Vertices[currentFace.B];
var vertexC = cMesh.Vertices[currentFace.C];
var pixelA = this.project(vertexA, transformMatrix);
var pixelB = this.project(vertexB, transformMatrix);
var pixelC = this.project(vertexC, transformMatrix);
var color: number = 0.25 + ((indexFaces % cMesh.Faces.length) / cMesh.Faces.length) * 0.75;
this.drawTriangle(pixelA, pixelB, pixelC, new BABYLON.Color4(color, color, color, 1));
}
~~~
【譯者注:JavaScript代碼】
~~~
for (var indexFaces = 0; indexFaces < cMesh.Faces.length; indexFaces++) {
var currentFace = cMesh.Faces[indexFaces];
var vertexA = cMesh.Vertices[currentFace.A];
var vertexB = cMesh.Vertices[currentFace.B];
var vertexC = cMesh.Vertices[currentFace.C];
var pixelA = this.project(vertexA, transformMatrix);
var pixelB = this.project(vertexB, transformMatrix);
var pixelC = this.project(vertexC, transformMatrix);
var color = 0.25 + ((indexFaces % cMesh.Faces.length) / cMesh.Faces.length) * 0.75;
this.drawTriangle(pixelA, pixelB, pixelC, new BABYLON.Color4(color, color, color, 1));
}
~~~
結果應該是這樣的:
[運行代碼](http://david.blob.core.windows.net/softengine3d/part4sample1/index.html)
這是怎么回事?為什么感覺這么奇怪?!嗯~這是因為我們沒有正確的把正面的三角形畫在正面。【譯者注:我是這么翻譯的,你就這么一看~】
如何使用深度緩沖
我們需要對當前的Z值在緩沖區中進行比較。
如果當前要繪制的像素Z值是最前面的(最靠近屏幕),則可以繪制。
然而,如果當前Z值大于前面的像素,則可以被丟棄。
我們需要一個東西用來保存深度緩沖區。因此,我們聲明一個新的數組,并將其命名為深度緩沖區(depthBuffer)。該數組的大小等于 屏幕(width * height)。
每次調用 clear() 函數時深度緩沖區內的每一個元素都需要一個非常高的默認Z值。
在putPixel(函數/方法)中,我們需要測試已存儲在緩沖區中某個指定的像素Z值。此外,我們以前的邏輯接收Vector2用于繪制在屏幕上。現在我們將其改為Vector3用于增加Z值。因為現在我們將需要這部分新信息用于正確繪制面片。
最后,在我們的三角形內,同樣需要一個類似 x 值插值的方式對 z 值進行插值。
總之,這里是你所需要對設備對象更新的代碼:
【譯者注:C#代碼】
~~~
private byte[] backBuffer;
private readonly float[] depthBuffer;
private WriteableBitmap bmp;
private readonly int renderWidth;
private readonly int renderHeight;
public Device(WriteableBitmap bmp)
{
this.bmp = bmp;
renderWidth = bmp.PixelWidth;
renderHeight = bmp.PixelHeight;
// 后臺緩沖區大小值是要繪制的像素
// 屏幕(width*height) * 4 (R,G,B & Alpha值)
backBuffer = new byte[bmp.PixelWidth * bmp.PixelHeight * 4];
depthBuffer = new float[bmp.PixelWidth * bmp.PixelHeight];
}
// 清除后臺緩沖區為指定顏色
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;
}
// 清除深度緩沖區
for (var index = 0; index < depthBuffer.Length; index++)
{
depthBuffer[index] = float.MaxValue;
}
}
// 調用此方法把一個像素繪制到指定的X, Y坐標上
public void PutPixel(int x, int y, float z, Color4 color)
{
// 我們的后臺緩沖區是一維數組
// 這里我們簡單計算,將X和Y對應到此一維數組中
var index = (x + y * renderWidth);
var index4 = index * 4;
if (depthBuffer[index] < z)
{
return; // 深度測試不通過
}
depthBuffer[index] = z;
backBuffer[index4] = (byte)(color.Blue * 255);
backBuffer[index4 + 1] = (byte)(color.Green * 255);
backBuffer[index4 + 2] = (byte)(color.Red * 255);
backBuffer[index4 + 3] = (byte)(color.Alpha * 255);
}
// 將三維坐標和變換矩陣轉換成二維坐標
public Vector3 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 Vector3(x, y, point.Z));
}
// 如果二維坐標在可視范圍內則繪制
public void DrawPoint(Vector3 point, Color4 color)
{
// 判斷是否在屏幕內
if (point.X >= 0 && point.Y >= 0 && point.X < bmp.PixelWidth && point.Y < bmp.PixelHeight)
{
// 繪制一個點
PutPixel((int)point.X, (int)point.Y, point.Z, color);
}
}
// 在兩點之間從左到右繪制一條線段
// papb -> pcpd
// pa, pb, pc, pd在之前必須已經排好序
void ProcessScanLine(int y, Vector3 pa, Vector3 pb, Vector3 pc, Vector3 pd, Color4 color)
{
// 由當前的y值,我們可以計算出梯度
// 以此再計算出 起始X(sx) 和 結束X(ex)
// 如果pa.Y == pb.Y 或者 pc.Y== pd.y的話,梯度強制為1
var gradient1 = pa.Y != pb.Y ? (y - pa.Y) / (pb.Y - pa.Y) : 1;
var gradient2 = pc.Y != pd.Y ? (y - 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);
// 從左(sx)向右(ex)繪制一條線
for (var x = sx; x < ex; x++)
{
float gradient = (x - sx) / (float)(ex - sx);
var z = Interpolate(z1, z2, gradient);
DrawPoint(new Vector3(x, y, z), color);
}
}
~~~
【譯者注:TypeScript代碼】
~~~
// 后臺緩沖區大小值是要繪制的像素
// 屏幕(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;
private depthbuffer: number[];
constructor(canvas: HTMLCanvasElement) {
this.workingCanvas = canvas;
this.workingWidth = canvas.width;
this.workingHeight = canvas.height;
this.workingContext = this.workingCanvas.getContext("2d");
this.depthbuffer = new Array(this.workingWidth * this.workingHeight);
}
// 用指定顏色清除后臺緩沖區
public clear(): void {
// 使用默認顏色清除后臺緩沖區
this.workingContext.clearRect(0, 0, this.workingWidth, this.workingHeight);
// 緩存后臺緩沖區
this.backbuffer = this.workingContext.getImageData(0, 0, this.workingWidth, this.workingHeight);
// 清除深度緩沖區
for (var i = 0; i < this.depthbuffer.length; i++) {
// 使用最大值填充
this.depthbuffer[i] = 10000000;
}
}
// 調用此方法把一個像素繪制到指定的X, Y坐標上
public putPixel(x: number, y: number, z: number, color: BABYLON.Color4): void {
this.backbufferdata = this.backbuffer.data;
// 我們的后臺緩沖區是一維數組
// 這里我們簡單計算,將X和Y對應到此一維數組中
var index: number = ((x >> 0) + (y >> 0) * this.workingWidth);
var index4: number = index * 4;
if (this.depthbuffer[index] < z) {
return; // 深度測試不通過
}
this.depthbuffer[index] = z;
// 在Html5 canvas中使用RGBA顏色空間
this.backbufferdata[index4] = color.r * 255;
this.backbufferdata[index4 + 1] = color.g * 255;
this.backbufferdata[index4 + 2] = color.b * 255;
this.backbufferdata[index4 + 3] = color.a * 255;
}
// 將三維坐標和變換矩陣轉換成二維坐標
public project(coord: BABYLON.Vector3, transMat: BABYLON.Matrix): BABYLON.Vector3 {
// 進行坐標變換
var point = BABYLON.Vector3.TransformCoordinates(coord, transMat);
// 變換后的坐標起始點是坐標系的中心點
// 但是,在屏幕上,我們以左上角為起始點
// 我們需要重新計算使他們的起始點變成左上角
var x = point.x * this.workingWidth + this.workingWidth / 2.0;
var y = -point.y * this.workingHeight + this.workingHeight / 2.0;
return (new BABYLON.Vector3(x, y, point.z));
}
// 如果二維坐標在可視范圍內則繪制
public drawPoint(point: BABYLON.Vector3, color: BABYLON.Color4): void {
// 判斷是否在屏幕內
if (point.x >= 0 && point.y >= 0 && point.x < this.workingWidth && point.y < this.workingHeight) {
// 繪制一個點
this.putPixel(point.x, point.y, point.z, color);
}
}
// 在兩點之間從左到右繪制一條線段
// papb -> pcpd
// pa, pb, pc, pd在之前必須已經排好序
public processScanLine(y: number, pa: BABYLON.Vector3, pb: BABYLON.Vector3, pc: BABYLON.Vector3, pd: BABYLON.Vector3, color: BABYLON.Color4): void {
// 由當前的y值,我們可以計算出梯度
// 以此再計算出 起始X(sx) 和 結束X(ex)
// 如果pa.Y == pb.Y 或者 pc.Y== pd.y的話,梯度強制為1
var gradient1 = pa.y != pb.y ? (y - pa.y) / (pb.y - pa.y) : 1;
var gradient2 = pc.y != pd.y ? (y - 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);
// 從左(sx)向右(ex)繪制一條線
for (var x = sx; x < ex; x++) {
var gradient: number = (x - sx) / (ex - sx); // 規范從左往右繪制
var z = this.interpolate(z1, z2, gradient);
this.drawPoint(new BABYLON.Vector3(x, y, z), color);
}
}
~~~
【譯者注:JavaScript代碼】
~~~
function Device(canvas) {
this.workingCanvas = canvas;
this.workingWidth = canvas.width;
this.workingHeight = canvas.height;
this.workingContext = this.workingCanvas.getContext("2d");
this.depthbuffer = new Array(this.workingWidth * this.workingHeight);
}
// 用指定顏色清除后臺緩沖區
Device.prototype.clear = function () {
// 使用默認顏色清除后臺緩沖區
this.workingContext.clearRect(0, 0, this.workingWidth, this.workingHeight);
// 緩存后臺緩沖區
this.backbuffer = this.workingContext.getImageData(0, 0, this.workingWidth, this.workingHeight);
// 清除深度緩沖區
for (var i = 0; i < this.depthbuffer.length; i++) {
// 使用最大值填充
this.depthbuffer[i] = 10000000;
}
};
// 調用此方法把一個像素繪制到指定的X, Y坐標上
Device.prototype.putPixel = function (x, y, z, color) {
this.backbufferdata = this.backbuffer.data;
// 我們的后臺緩沖區是一維數組
// 這里我們簡單計算,將X和Y對應到此一維數組中
var index = ((x >> 0) + (y >> 0) * this.workingWidth);
var index4 = index * 4;
if (this.depthbuffer[index] < z) {
return; // 深度測試不通過
}
this.depthbuffer[index] = z;
// 在Html5 canvas中使用RGBA顏色空間
this.backbufferdata[index4] = color.r * 255;
this.backbufferdata[index4 + 1] = color.g * 255;
this.backbufferdata[index4 + 2] = color.b * 255;
this.backbufferdata[index4 + 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;
var y = -point.y * this.workingHeight + this.workingHeight / 2.0;
return (new BABYLON.Vector3(x, y, point.z));
};
// 如果二維坐標在可視范圍內則繪制
Device.prototype.drawPoint = function (point, color) {
// 判斷是否在屏幕內
if (point.x >= 0 && point.y >= 0 && point.x < this.workingWidth && point.y < this.workingHeight) {
// 繪制一個點
this.putPixel(point.x, point.y, point.z, color);
}
};
// 在兩點之間從左到右繪制一條線段
// papb -> pcpd
// pa, pb, pc, pd在之前必須已經排好序
Device.prototype.processScanLine = function (y, pa, pb, pc, pd, color) {
// 由當前的y值,我們可以計算出梯度
// 以此再計算出 起始X(sx) 和 結束X(ex)
// 如果pa.Y == pb.Y 或者 pc.Y== pd.y的話,梯度強制為1
var gradient1 = pa.y != pb.y ? (y - pa.y) / (pb.y - pa.y) : 1;
var gradient2 = pc.y != pd.y ? (y - 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);
// 從左(sx)向右(ex)繪制一條線
for (var x = sx; x < ex; x++) {
var gradient = (x - sx) / (ex - sx);
var z = this.interpolate(z1, z2, gradient);
this.drawPoint(new BABYLON.Vector3(x, y, z), color);
}
};
~~~
使用這些新代碼,你將獲得的效果:
[運行代碼](http://david.blob.core.windows.net/softengine3d/part4/index.html)
同樣的,你可以下載源代碼:
C#:[SoftEngineCSharpPart4.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineCSharpPart4.zip)
TypeScript:[SoftEngineTSPart4.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineTSPart4.zip)
JavaScript:[SoftEngineJSPart4.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineJSPart4.zip)?或只需右鍵點擊 -> 查看框架的源代碼
在下一章節,第五個教程中,我們將看到如何模擬光照著色效果,我們將得到這樣的畫面:
