飞码网-免费源码博客分享网站

点击这里给我发消息

用JavaScript构建3D引擎|-JavaScript教程

飞码网-免费源码博客分享网站 爱上飞码网—https://www.codefrees.com— 飞码网-matlab-python-C++ 爱上飞码网—https://www.codefrees.com— 飞码网-免费源码博客分享网站

 

本教程的目的是解释如何在没有WebGL的情况下为Web构建简单的3D引擎。我们首先将看到如何存储3D形状。然后,我们将看到如何在两个不同的视图中显示这些形状。

存储和转换3D形状

所有形状都是多面体

虚拟世界与真实世界的主要不同之处在于:没有什么是连续的,而一切都是离散的。例如,您不能在屏幕上显示一个完美的圆圈。您可以通过绘制带有很多边的规则多边形来实现它:您拥有的边越多,圆就越“完美”。

在3D中,这是同一件事,必须使用与多边形等效的3D逼近每种形状:多面体(3D形状,在该3D形状中,我们仅发现平面而不是球形的弯曲边)。当我们谈论已经是多面体的形状(例如立方体)时,这并不奇怪,但是当我们要显示其他形状(例如球体)时,要牢记这一点。

领域

储存多面体

要猜测如何存储多面体,我们必须记住如何在数学中识别这种事物。您肯定在您的学年期间已经做过一些基本的几何设计。要确定一个正方形,例如,你怎么称呼它ABCD,有ABCD指顶点组成的正方形的每个角落。

对于我们的3D引擎,将是相同的。我们将从存储形状的每个顶点开始。然后,此形状将列出其面,并且每个面都将列出其顶点。

要表示一个顶点,我们需要正确的结构。在这里,我们创建一个类来存储顶点的坐标。

var Vertex = function(x, y, z) {
    this.x = parseFloat(x);
    this.y = parseFloat(y);
    this.z = parseFloat(z);
};

现在可以像创建任何其他对象一样创建一个顶点:

var A = new Vertex(10, 20, 0.5);

接下来,我们创建一个代表多面体的类。让我们以一个多维数据集为例。该类的定义在下面,并在后面进行解释。

var Cube = function(center, size) {
    // Generate the vertices
    var d = size / 2;

    this.vertices = [
        new Vertex(center.x - d, center.y - d, center.z + d),
        new Vertex(center.x - d, center.y - d, center.z - d),
        new Vertex(center.x + d, center.y - d, center.z - d),
        new Vertex(center.x + d, center.y - d, center.z + d),
        new Vertex(center.x + d, center.y + d, center.z + d),
        new Vertex(center.x + d, center.y + d, center.z - d),
        new Vertex(center.x - d, center.y + d, center.z - d),
        new Vertex(center.x - d, center.y + d, center.z + d)
    ];

    // Generate the faces
    this.faces = [
        [this.vertices[0], this.vertices[1], this.vertices[2], this.vertices[3]],
        [this.vertices[3], this.vertices[2], this.vertices[5], this.vertices[4]],
        [this.vertices[4], this.vertices[5], this.vertices[6], this.vertices[7]],
        [this.vertices[7], this.vertices[6], this.vertices[1], this.vertices[0]],
        [this.vertices[7], this.vertices[0], this.vertices[3], this.vertices[4]],
        [this.vertices[1], this.vertices[6], this.vertices[5], this.vertices[2]]
    ];
};

使用此类,我们可以通过指示其中心和边缘的长度来创建虚拟立方体。

var cube = new Cube(new Vertex(0, 0, 0), 200);

Cube该类的构造函数从生成立方体的顶点开始,该顶点是根据指示的中心的位置计算得出的。模式将更加清晰,因此请参见下面生成的八个顶点的位置:

 

 

立方体

然后,我们列出这些面孔。每个面都是一个正方形,因此我们需要为每个面指定四个顶点。在这里,我选择用数组表示一张脸,但是,如果需要,您可以为此创建一个专用类。

创建面时,我们使用四个顶点。因为它们存储在this.vertices[i]对象中,所以我们无需再次指出它们的位置这很实用,但是我们这样做还有另一个原因。

默认情况下,JavaScript尝试使用最少的内存。为此,它不会复制作为函数参数传递甚至存储到数组中的对象。对于我们来说,这是完美的行为。

 

实际上,每个顶点包含三个数字(它们的坐标),如果需要将它们相加,还会添加几种方法。如果对于每个面,我们存储顶点的副本,则将使用大量内存,这是无用的。在这里,我们所拥有的只是引用:坐标(和其他方法)仅存储一次,而仅存储一次。由于每个顶点由三个不同的面使用,因此通过存储引用而不是副本,我们将所需的内存除以三(或多或少)!

我们需要三角形吗?

如果您已经玩过3D(例如使用Blender之类的软件或使用WebGL之类的库),也许您听说过我们应该使用三角形。在这里,我选择不使用三角形。

之所以选择该选项,是因为本文是对该主题的介绍,我们将显示诸如立方体之类的基本形状。在我们的案例中,使用三角形来显示正方形比其他任何事情都要复杂。

但是,如果计划构建更完整的渲染器,则通常需要知道三角形是首选。这有两个主要原因:

  1. 纹理:出于某些数学原因,要在脸上显示图像,我们需要三角形。
  2. 怪异的面孔:三个顶点始终在同一平面上。但是,您可以添加不在同一平面上的第四个顶点,并且可以创建连接这四个顶点的面。在这种情况下,要绘制它,我们别无选择:我们必须将它分成两个三角形(只需尝试用一张纸!)。通过使用三角形,您可以保留控件并选择拆分发生的位置(感谢蒂姆的提醒!)。

代理多面体

存储引用而不是副本还有另一个优势。当我们想要修改多面体时,使用这样的系统还将需要的操作数除以三。

要了解原因,让我们再次回想一下我们的数学课。当您要翻译一个正方形时,实际上并不需要翻译。实际上,您平移了四个顶点,然后加入了平移。

在这里,我们将做同样的事情:我们不会碰到脸。我们在每个顶点上应用所需的操作,我们就完成了。当面使用参考时,面的坐标会自动更新。例如,查看如何转换先前创建的多维数据集:

for (var i = 0; i < 8; ++i) {
    cube.vertices[i].x += 50;
    cube.vertices[i].y += 20;
    cube.vertices[i].z += 15;
}

渲染图像

我们知道如何存储3D对象以及如何对其进行操作。现在是时候看看如何查看它们了!但是,首先,我们需要理论上的一些背景知识,以便理解我们将要做什么。

投影

目前,我们存储3D坐标。但是,屏幕只能显示2D坐标,因此我们需要一种将3D坐标转换为2D坐标的方法:这就是我们所说的数学投影。3D到2D投影是由称为虚拟相机的新对象进行的抽象操作。该摄像机拍摄3D对象,并将其坐标转换为2D对象,然后将其发送到渲染器,渲染器将在屏幕上显示它们。我们将在这里假定摄像机位于3D空间的原点(因此其坐标为(0,0,0))。

 

 

由于这篇文章中我们已经谈到坐标的开始,代表由三个数字:xyz但是要定义坐标,我们需要一个基础:z垂直坐标是吗?它是到达顶部还是底部?没有通用的答案,也没有约定,因为事实是您可以选择所需的任何东西。您唯一需要记住的是,当您对3D对象执行操作时,您必须保持一致,因为公式会根据3D对象而改变。在本文中,我选择了可​​以在上面的多维数据集的架构中看到的基础:x从左到右,y从后到前以及z从下到上。

现在,我们知道该怎么做:我们在(x,y,z)基础中有坐标,并且要显示它们,我们需要将它们转换为(x,z)基础中的坐标:因为它是一个平面,所以我们将能够显示它们。

不仅有一个投影。更糟糕的是,存在无数种不同的投影!在本文中,我们将看到两种不同类型的投影,它们实际上是最常用的一种。

如何渲染场景

在投影我们的对象之前,让我们写一个显示它们的函数。此函数接受一个数组作为参数,该数组列出要渲染的对象,必须用于显示对象的画布的上下文以及在正确位置绘制对象所需的其他详细信息。

该数组可以包含多个要渲染的对象。这些对象必须尊重一件事:具有一个名为属性的公共属性faces,该属性是一个列出对象所有面的数组(例如我们先前创建的多维数据集)。这些面可以是任何东西(如果需要,可以是正方形,三角形甚至十二边形):它们只需要是列出其顶点的数组即可。

让我们看一下该函数的代码,然后进行解释:

function render(objects, ctx, dx, dy) {
    // For each object
    for (var i = 0, n_obj = objects.length; i < n_obj; ++i) {
        // For each face
        for (var j = 0, n_faces = objects[i].faces.length; j < n_faces; ++j) {
            // Current face
            var face = objects[i].faces[j];

            // Draw the first vertex
            var P = project(face[0]);
            ctx.beginPath();
            ctx.moveTo(P.x + dx, -P.y + dy);

            // Draw the other vertices
            for (var k = 1, n_vertices = face.length; k < n_vertices; ++k) {
                P = project(face[k]);
                ctx.lineTo(P.x + dx, -P.y + dy);
            }

            // Close the path and draw the face
            ctx.closePath();
            ctx.stroke();
            ctx.fill();
        }
    }
}

此功能值得一些解释。更确切地说,我们需要解释这个project()函数什么,以及这些函数dxdy参数是什么剩下的基本上就是列出对象,然后绘制每个面。

顾名思义,project()此处功能是将3D坐标转换为2D坐标。它在3D空间中接受一个顶点,并在2D平面中返回一个我们可以如下定义的顶点。

var Vertex2D = function(x, y) {
    this.x = parseFloat(x);
    this.y = parseFloat(y);
};

我没有在这里命名坐标x而是在z这里将z坐标重命名y,以保持我们在2D几何中经常发现的经典约定,但是z如果愿意,可以保留

确切的内容project()是我们将在下一部分中看到的内容:它取决于您选择的投影类型。但是无论这种类型是什么,该render()功能都可以保持原样。

一旦我们在平面上有了坐标,我们就可以在画布上显示它们,这就是我们要做的……有一点技巧:我们实际上并没有绘制project()函数返回的实际坐标

实际上,该project()函数返回虚拟2D平面上的坐标,但其原点与我们为3D空间定义的原点相同。但是,我们希望的来源是在我们的画布的中心,这就是为什么我们在翻译的坐标:顶点(0,0)是不是在画布的中心,但是(0 + dx,0 + dy),如果我们选择dxdy明智的。由于我们希望(dx,dy)位于画布的中心,因此我们没有真正的选择,而是定义了dx = canvas.width / 2dy = canvas.height / 2

最后,最后一个细节:我们为什么直接使用-y而不是y直接使用?答案取决于我们的选择基础:z轴指向顶部。然后,在我们的场景中,z坐标为正的顶点将向上移动。但是,在画布上,y轴指向底部:y坐标为正的顶点将向下移动。这就是为什么我们需要在画布上将画布的y坐标定义为场景的z坐标的反方向。

 

 

既然render()功能已经很清楚了,该看一下了project()

正交视图

让我们从正交投影开始。因为这是最简单的,所以了解我们将要做的事情是完美的。

我们有三个坐标,而我们只想要两个。在这种情况下最简单的事情是什么?删除坐标之一。这就是我们在正交视图中所做的。我们将删除代表深度的y坐标坐标。

function project(M) {
    return new Vertex2D(M.x, M.z);
}

现在,您可以测试自本文开始以来我们编写的所有代码:它可以工作!恭喜,您刚刚在平面屏幕上显示了3D对象!

此功能在下面的实时示例中实现,在该示例中,您可以通过使用鼠标旋转多维数据集来与多维数据集进行交互。

请参阅CodePen上的SitePoint(@SitePoint)的Pen 3D正交视图。

 

有时候,我们想要的是正交视图,因为它具有保留相似之处的优势。但是,这不是最自然的视图:我们的眼睛看不到那样的景象。这就是为什么我们将看到第二个投影:透视图的原因。

透视图

透视图比正交视图稍微复杂一点,因为我们需要进行一些计算。但是,这些计算并不那么复杂,您只需要知道一件事:如何使用截距定理。

为了理解原因,让我们看一下表示正交视图的模式。我们以正交方式将点投影在平面上。

正交视图

但是,在现实生活中,我们的眼睛的行为更像是以下模式。

透视图

 

 

基本上,我们有两个步骤:

  1. 我们将原始顶点和相机的原点连接在一起;
  2. 投影是该线和平面之间的交点。

与正交视图相反,此处的平面的精确位置很重要:如果将平面远离相机放置,将不会获得与将其放置在靠近相机时相同的效果。在这里,我们将其放置在距d相机一定距离的位置。

M(x,y,z)3D空间中的顶点开始,我们要计算平面上(x',z')投影的坐标M'

透视投影

为了猜测我们将如何计算这些坐标,让我们从另一个角度来看,并看到与上面相同的模式,但是从顶部看。

从顶部透视投影

我们可以识别出截距定理中使用的配置。在上述模式中,我们知道一些值:xyd,等等。我们要进行计算,x'因此我们应用截距定理并获得以下等式:x' = d / y * x

现在,如果你看一下从一个侧面同样的场景,你会得到一个类似的模式,可以让您获得的价值z'感谢zydz' = d / y * z

现在,我们可以project()使用透视图编写函数:

function project(M) {
    // Distance between the camera and the plane
    var d = 200;
    var r = d / M.y;

    return new Vertex2D(r * M.x, r * M.z);
}

可以在下面的实时示例中测试此功能。再一次,您可以与多维数据集进行交互。

请参阅CodePen上的SitePoint(@SitePoint)提供的Pen 3D透视图。

 

结束语

我们的(非常基本的)3D引擎现在可以显示我们想要的任何3D形状。您可以采取一些措施来增强它。例如,我们看到形状的每个面,甚至背面的面。要隐藏它们,可以实施背面剔除。

另外,我们没有谈论纹理。在这里,我们所有的形状共享相同的颜色。您可以通过例如color在对象中添加属性来更改它,以了解如何绘制它们。您甚至可以为每张脸选择一种颜色,而无需进行很多更改。您也可以尝试在脸上显示图像。但是,这比较困难,并且详细说明如何执行此操作将花费整篇文章。

其他事情可以更改。我们将相机放置在空间的原点,但是您可以移动它(在投影顶点之前需要更改基准)。另外,在此处绘制了放置在摄像机后面的顶点,这不是我们想要的。剪切平面可以解决此问题(易于理解,难以实现)。

如您所见,我们在这里构建的3D引擎远远不够完善,这也是我自己的解释。您可以与其他类一起添加自己的风格:例如,Three.js使用专用的类来管理摄像机和投影。另外,我们使用基本的数学运算来存储坐标,但是,如果您想创建一个更复杂的应用程序,并且例如在一个帧中需要旋转很多顶点,那么您将不会有流畅的体验。要对其进行优化,您将需要一些更复杂的数学运算:齐次坐标(射影几何)和四元数。

 

飞码网-免费源码博客分享网站 爱上飞码网—https://www.codefrees.com— 飞码网-matlab-python-C++ 爱上飞码网—https://www.codefrees.com— 飞码网-免费源码博客分享网站
赞 ()
内容页底部广告位3
留言与评论(共有 0 条评论)
   
验证码: