现在,由于有了前面的教程第1部分 ,我们已经构建了3D引擎的核心,我们可以致力于增强渲染。 然后,下一步是连接点以绘制一些线,以渲染您可能称为“线框”的渲染 。
1 – 编写相机,网格和设备对象的核心逻辑
2 –绘制线条和三角形以获得线框渲染(本文)
3 – 加载从Blender以JSON格式导出的网格
4 – 用栅格化和使用Z缓冲区填充三角形
4b – 奖金:使用技巧和并行性来提高性能
5 – 使用平面阴影和Gouraud阴影处理光
6 – 应用纹理,背面剔除和WebGL
在本教程中,您将学习如何绘制线条,什么是面以及使用Bresenham算法绘制一些三角形有多酷。
因此,最后,您将知道如何编写像这样的酷代码:
是! 我们的3D旋转立方体开始真正出现在我们的屏幕上!
第一种在两点之间画线的基本算法
让我们从编码一个简单的算法开始。 为了在2个顶点之间画一条线 ,我们将使用以下逻辑:
–如果两个点(point0和point1)之间的距离小于2像素,则无事可做
–否则,我们将找到两个点之间的中间点 (point0坐标+(point1坐标– point0坐标)/ 2)
–我们正在屏幕上绘制该点
–我们将在point0和中点之间以及中点和point1之间递归启动此算法
这是执行此操作的代码:
public void DrawLine( Vector2 point0, Vector2 point1) var dist = (point1 - point0).Length(); // If the distance between the 2 points is less than 2 pixels // We're exiting if (dist < 2) return ; // Find the middle point between first & second point Vector2 middlePoint = point0 + (point1 - point0)/2; // We draw this point on screen DrawPoint(middlePoint); // Recursive algorithm launched between first & middle point // and between middle & second point DrawLine(point0, middlePoint); DrawLine(middlePoint, point1);
public drawLine(point0: BABYLON.Vector2, point1: BABYLON.Vector2): void { var dist = point1.subtract(point0).length(); // If the distance between the 2 points is less than 2 pixels // We're exiting if (dist < 2) return ; // Find the middle point between first & second point var middlePoint = point0.add((point1.subtract(point0)).scale(0.5)); // We draw this point on screen this .drawPoint(middlePoint); // Recursive algorithm launched between first & middle point // and between middle & second point this .drawLine(point0, middlePoint); this .drawLine(middlePoint, point1);
Device.prototype.drawLine = function (point0, point1) { var dist = point1.subtract(point0).length(); // If the distance between the 2 points is less than 2 pixels // We're exiting if (dist < 2) { return ; } // Find the middle point between first & second point var middlePoint = point0.add((point1.subtract(point0)).scale(0.5)); // We draw this point on screen this .drawPoint(middlePoint); // Recursive algorithm launched between first & middle point // and between middle & second point this .drawLine(point0, middlePoint); this .drawLine(middlePoint, point1); ;
您需要更新渲染循环以使用以下新代码:
for ( var i = 0; i < mesh.Vertices.Length - 1; i++) var point0 = Project(mesh.Vertices[i], transformMatrix); var point1 = Project(mesh.Vertices[i + 1], transformMatrix); DrawLine(point0, point1);
for ( var i = 0; i < cMesh.Vertices.length -1; i++){ var point0 = this .project(cMesh.Vertices[i], transformMatrix); var point1 = this .project(cMesh.Vertices[i + 1], transformMatrix); this .drawLine(point0, point1);
for ( var i = 0; i < cMesh.Vertices.length -1; i++){ var point0 = this .project(cMesh.Vertices[i], transformMatrix); var point1 = this .project(cMesh.Vertices[i + 1], transformMatrix); this .drawLine(point0, point1);
现在您应该获得类似的信息:
我知道这看起来很奇怪,但这是预期的行为。 它应该可以帮助您开始理解显示3D网格所需的操作。 但是要获得更好的渲染,我们需要发现一个新概念。
用三角形显示面
现在我们知道如何绘制线了,我们需要一种更好的方法来渲染带有它们的网格。 最简单的2D几何形状是三角形 。 3D的想法是使用这些三角形绘制所有网格。 然后,我们需要将立方体的每一侧拆分为2个三角形。 我们将“手动”执行此操作,但是在下一个教程中,我们将看到3D建模人员现在正在为我们自动执行此步骤。
要绘制三角形,您需要有3个点/顶点。 因此,面只是一个包含3个值的结构,这些值是指向要渲染的网格的适当顶点阵列的索引。
为了理解这个概念,让我们以Blender显示的多维数据集作为上图:
我们在此图上显示4个顶点,并带有以下索引:0、1、2、3。要绘制立方体的上侧,我们需要绘制2个三角形。 第一个面为Face 0,将绘制3条线,从顶点0 (-1、1、1)到顶点1 (1、1、1),从顶点1 (1、1、1)到顶点2 (- 1,–1,1),最后从顶点2 (-1,–1,1)到顶点0 (-1,1,1)。 将使用从顶点1到顶点2 ,顶点2到顶点3以及顶点3到顶点1的线绘制第二个三角形Face 1 。
等效的代码将是这样的:
var mesh = new SoftEngine. Mesh ( "Square" , 4, 2); eshes.Add(mesh); esh.Vertices[0] = new Vector3 (-1, 1, 1); esh.Vertices[1] = new Vector3 (1, 1, 1); esh.Vertices[2] = new Vector3 (-1, -1, 1); esh.Vertices[3] = new Vector3 (1, -1, 1); mesh.Faces[0] = new Face { A = 0, B = 1, C = 2 }; esh.Faces[1] = new Face { A = 1, B = 2, C = 3 };
如果要绘制整个立方体,则需要找到剩余的10 个面 ,因为要绘制立方体的6 个面有12个面 。
现在让我们为Face对象定义代码。 这是一个非常简单的对象,因为它只是3个索引的集合 。 这是Face和新的Mesh定义的代码,现在也可以使用它:
namespace SoftEngine public struct Face { public int A; public int B; public int C; } public class Mesh { public string Name { get ; set ; } public Vector3 [] Vertices { get ; private set ; } public Face [] Faces { get ; set ; } public Vector3 Position { get ; set ; } public Vector3 Rotation { get ; set ; } public Mesh( string name, int verticesCount, int facesCount) { Vertices = new Vector3 [verticesCount]; Faces = new Face [facesCount]; Name = name; } }
///<reference path="babylon.math.ts"/> module SoftEngine { export interface Face { A: number ; B: number ; C: number ; } export class Mesh { Position: BABYLON.Vector3; Rotation: BABYLON.Vector3; Vertices: BABYLON.Vector3[]; Faces: Face[]; constructor ( public name: string , verticesCount: number , facesCount: number ) { this .Vertices = new Array(verticesCount); this .Faces = new Array(facesCount); this .Rotation = new BABYLON.Vector3(0, 0, 0); this .Position = new BABYLON.Vector3(0, 0, 0); } }
var SoftEngine; function (SoftEngine) { var Mesh = ( function () { function Mesh(name, verticesCount, facesCount) { this .name = name; this .Vertices = new Array(verticesCount); this .Faces = new Array(facesCount); this .Rotation = new BABYLONTS.Vector3(0, 0, 0); this .Position = new BABYLONTS.Vector3(0, 0, 0); } return Mesh; })(); SoftEngine.Mesh = Mesh; )(SoftEngine || (SoftEngine = {}));
现在,我们需要更新Device对象的Render()函数/方法,以迭代定义的所有面并绘制关联的三角形。
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); DrawLine(pixelA, pixelB); DrawLine(pixelB, pixelC); DrawLine(pixelC, pixelA);
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); this .drawLine(pixelA, pixelB); this .drawLine(pixelB, pixelC); this .drawLine(pixelC, pixelA);
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); this .drawLine(pixelA, pixelB); this .drawLine(pixelB, pixelC); this .drawLine(pixelC, pixelA);
最后,我们需要使用12个面正确声明与多维数据集关联的网格,以使此新代码按预期工作。
这是新的声明:
var mesh = new SoftEngine. Mesh ( "Cube" , 8, 12); eshes.Add(mesh); esh.Vertices[0] = new Vector3 (-1, 1, 1); esh.Vertices[1] = new Vector3 (1, 1, 1); esh.Vertices[2] = new Vector3 (-1, -1, 1); esh.Vertices[3] = new Vector3 (1, -1, 1); esh.Vertices[4] = new Vector3 (-1, 1, -1); esh.Vertices[5] = new Vector3 (1, 1, -1); esh.Vertices[6] = new Vector3 (1, -1, -1); esh.Vertices[7] = new Vector3 (-1, -1, -1); mesh.Faces[0] = new Face { A = 0, B = 1, C = 2 }; esh.Faces[1] = new Face { A = 1, B = 2, C = 3 }; esh.Faces[2] = new Face { A = 1, B = 3, C = 6 }; esh.Faces[3] = new Face { A = 1, B = 5, C = 6 }; esh.Faces[4] = new Face { A = 0, B = 1, C = 4 }; esh.Faces[5] = new Face { A = 1, B = 4, C = 5 }; mesh.Faces[6] = new Face { A = 2, B = 3, C = 7 }; esh.Faces[7] = new Face { A = 3, B = 6, C = 7 }; esh.Faces[8] = new Face { A = 0, B = 2, C = 7 }; esh.Faces[9] = new Face { A = 0, B = 4, C = 7 }; esh.Faces[10] = new Face { A = 4, B = 5, C = 6 }; esh.Faces[11] = new Face { A = 4, B = 6, C = 7 };
var mesh = new SoftEngine.Mesh( "Cube" , 8, 12); eshes.push(mesh); esh.Vertices[0] = new BABYLON.Vector3(-1, 1, 1); esh.Vertices[1] = new BABYLON.Vector3(1, 1, 1); esh.Vertices[2] = new BABYLON.Vector3(-1, -1, 1); esh.Vertices[3] = new BABYLON.Vector3(1, -1, 1); esh.Vertices[4] = new BABYLON.Vector3(-1, 1, -1); esh.Vertices[5] = new BABYLON.Vector3(1, 1, -1); esh.Vertices[6] = new BABYLON.Vector3(1, -1, -1); esh.Vertices[7] = new BABYLON.Vector3(-1, -1, -1); mesh.Faces[0] = { A:0, B:1, C:2 }; esh.Faces[1] = { A:1, B:2, C:3 }; esh.Faces[2] = { A:1, B:3, C:6 }; esh.Faces[3] = { A:1, B:5, C:6 }; esh.Faces[4] = { A:0, B:1, C:4 }; esh.Faces[5] = { A:1, B:4, C:5 }; mesh.Faces[6] = { A:2, B:3, C:7 }; esh.Faces[7] = { A:3, B:6, C:7 }; esh.Faces[8] = { A:0, B:2, C:7 }; esh.Faces[9] = { A:0, B:4, C:7 }; esh.Faces[10] = { A:4, B:5, C:6 }; esh.Faces[11] = { A:4, B:6, C:7 };
var mesh = new SoftEngine.Mesh( "Cube" , 8, 12); eshes.push(mesh); esh.Vertices[0] = new BABYLON.Vector3(-1, 1, 1); esh.Vertices[1] = new BABYLON.Vector3(1, 1, 1); esh.Vertices[2] = new BABYLON.Vector3(-1, -1, 1); esh.Vertices[3] = new BABYLON.Vector3(1, -1, 1); esh.Vertices[4] = new BABYLON.Vector3(-1, 1, -1); esh.Vertices[5] = new BABYLON.Vector3(1, 1, -1); esh.Vertices[6] = new BABYLON.Vector3(1, -1, -1); esh.Vertices[7] = new BABYLON.Vector3(-1, -1, -1); mesh.Faces[0] = { A:0, B:1, C:2 }; esh.Faces[1] = { A:1, B:2, C:3 }; esh.Faces[2] = { A:1, B:3, C:6 }; esh.Faces[3] = { A:1, B:5, C:6 }; esh.Faces[4] = { A:0, B:1, C:4 }; esh.Faces[5] = { A:1, B:4, C:5 }; mesh.Faces[6] = { A:2, B:3, C:7 }; esh.Faces[7] = { A:3, B:6, C:7 }; esh.Faces[8] = { A:0, B:2, C:7 }; esh.Faces[9] = { A:0, B:4, C:7 }; esh.Faces[10] = { A:4, B:5, C:6 }; esh.Faces[11] = { A:4, B:6, C:7 };
您现在应该拥有一个漂亮的旋转立方体:
恭喜! :)
使用Bresenham增强线条绘制算法
有一种使用布雷森汉姆(Bresenham)线算法绘制线的最佳方法。 它比我们当前的简单递归版本更快更清晰。 该算法的故事很有趣。 请阅读Wikipedia对该算法的定义,以了解Bresenham如何构建该算法以及其原因。
以下是此算法在C#,TypeScript和JavaScript中的版本:
public void DrawBline( Vector2 point0, Vector2 point1) int x0 = ( int )point0.X; int y0 = ( int )point0.Y; int x1 = ( int )point1.X; int y1 = ( int )point1.Y; var dx = Math .Abs(x1 - x0); var dy = Math .Abs(y1 - y0); var sx = (x0 < x1) ? 1 : -1; var sy = (y0 < y1) ? 1 : -1; var err = dx - dy; while ( true ) { DrawPoint( new Vector2 (x0, y0)); if ((x0 == x1) && (y0 == y1)) break ; var e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } }
public drawBline(point0: BABYLON.Vector2, point1: BABYLON.Vector2): void { var x0 = point0.x >> 0; var y0 = point0.y >> 0; var x1 = point1.x >> 0; var y1 = point1.y >> 0; var dx = Math.abs(x1 - x0); var dy = Math.abs(y1 - y0); var sx = (x0 < x1) ? 1 : -1; var sy = (y0 < y1) ? 1 : -1; var err = dx - dy; while ( true ) { this .drawPoint( new BABYLON.Vector2(x0, y0)); if ((x0 == x1) && (y0 == y1)) break ; var e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } }
Device.prototype.drawBline = function (point0, point1) { var x0 = point0.x >> 0; var y0 = point0.y >> 0; var x1 = point1.x >> 0; var y1 = point1.y >> 0; var dx = Math.abs(x1 - x0); var dy = Math.abs(y1 - y0); var sx = (x0 < x1) ? 1 : -1; var sy = (y0 < y1) ? 1 : -1; var err = dx - dy; while ( true ) { this .drawPoint( new BABYLON.Vector2(x0, y0)); if ((x0 == x1) && (y0 == y1)) break ; var e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } ;
在render函数中,将调用do DrawLine替换为DrawBline,您应该注意到这更加流畅并且更加清晰:
如果您注意观察它,应该看到使用Bresenham的该版本比我们的第一个算法更不连贯。
同样,您可以下载包含源代码的解决方案 :
– C# : SoftEngineCSharpPart2.zip
– TypeScript : SoftEngineTSPart2.zip
– JavaScript : SoftEngineJSPart2.zip或直接右键单击–>在嵌入式iframe上查看源
在下一个教程中,您将学习如何从Blender (一种免费的3D建模工具)中将某些网格导出为JSON格式。 然后,我们将加载此JSON文件,以使用线框引擎显示该文件 。 确实,我们已经进行了所有设置,以显示更复杂的网格,如下所示:
第三部分见。
最初发布: http : //blogs.msdn.com/b/davrous/archive/2013/06/14/tutorial-part-2-learning-how-to-write-a-3d-soft-engine-from-scratch在C-TS或JS绘图线-amp-triangles.aspx中 。 经作者许可在此处转载。
From: https://www.sitepoint.com/write-3d-soft-engine-scratch-part-2/