65.9K
CodeProject 正在变化。 阅读更多。
Home

CurSur - WebGL中的几何设计3D曲线和曲面

starIconstarIconstarIconstarIconstarIcon

5.00/5 (25投票s)

2021年2月18日

CPOL

21分钟阅读

viewsIcon

31505

downloadIcon

786

使用Three.js库实现的几何设计中三种类型的曲线和三种类型的曲面——三次曲线、贝塞尔曲线和B样条曲线。

 

1. 引言

5295155/Img01.png

您可以在https://amarnaths0005.github.io/3DCurvesSurfaces/#two[^]上运行该应用程序。

您也可以在Github站点下载代码

.

在几何设计中,有多种类型的曲线和曲面——包括2D和3D。其中,在几何建模的入门课程中,通常会教授三种类型——三次曲线、贝塞尔曲线和B样条曲线及曲面。本文的目标是演示和解释一个程序,该程序使用名为Three.js的WebGL 3D JavaScript库,在浏览器中绘制这些3D曲线和曲面。

此应用程序名为CurSur,是Curves and Surfaces的缩写。

在本文中,我将演示如何使用Three.js在浏览器中绘制3D线条和曲面,以及如何修改这些曲线和曲面的几何参数,如控制点和切线(导数)。此处代码的主要要求是:

  1. 应在屏幕上显示这六种类型的3D曲线和曲面——参数三次曲线、Coons双三次曲面、贝塞尔曲线、贝塞尔曲面、B样条(实际上是NURBS)曲线和NURBS曲面。
  2. 应允许用户修改控制点的x、y、z坐标,和/或曲线或曲面相对于x、y、z的导数(切线),并动态查看曲线或曲面在屏幕上的修改情况。
  3. 应允许用户以线框模式查看曲面。
  4. 应显示曲线或曲面的边界框,其尺寸为2个单位,以原点为中心。
  5. 应允许用户修改摄像机角度,使摄像机围绕正在查看的场景沿垂直轴旋转。
  6. 应允许用户修改参数值——曲线的u,曲面的u, w,并观察随着这些u, w值的变化,相应的点在曲线和曲面上移动。
  7. 应在单击按钮时显示一些标准曲线和曲面。
  8. 应使用Three.js库在屏幕上显示3D曲线或曲面。
  9. 应使用原生JS,不使用任何框架。
  10. 不应有文本框类型的用户输入,所有用户交互应仅通过滑块、复选框、组合框和按钮进行。
  11. 2023年2月新增:应允许用户输入图像,并将该图像显示为渲染曲面上的纹理。并更新至Three.js的第149版。

2. 参数曲线和曲面简介

作为学生,我们在高中和大学预科课程中学习到,直线、圆弧、圆锥曲线等线条的表示有笛卡尔坐标和极坐标形式。然而,出于几何设计的目的,笛卡尔坐标和极坐标形式通常不被首选,原因有二:(i)在笛卡尔坐标形式下,表示垂直或接近垂直的线不容易,因为斜率(导数)趋于无穷大;(ii)在这些笛卡尔坐标和极坐标形式下,表示一般形状不容易。因此,首选的形式是参数形式。

在参数形式中,3D曲线表示为

x = x(u)
y = y(u)
z = z(u)

其中x, y, z是曲线上任意点的3D坐标,u是一个参数,通常在0 ≤ u ≤ 1的范围内。其中x(u), y(u), z(u)是参数u的三个函数。通过这种表示,可以轻松表示任何形状的线,并且不会出现像笛卡尔表示中无限导数那样的缺点。特别是,我们看到了这些x(u), y(u), z(u)的三个函数,它们定义了本文中的三种曲线类型——参数三次曲线、贝塞尔曲线和B样条曲线。

以类似的方式,也可以使用以下方程表示曲面

x = x(u,w)
y = y(u,w)
z = z(u,w)

其中,照例,x, y, z是曲面上一点的3D坐标,u, w是参数空间中两个相互垂直方向的参数,通常在0 ≤ u ≤ 10 ≤ w ≤ 1的范围内。在这里,三个函数x(u,w), y(u,w), z(u,w)对于双三次、贝塞尔和B样条表示采用三种不同的形式。

3. 在Three.js中绘制

曲线由若干直线段绘制而成,这些直线段首尾相接,呈现出平滑曲线的外观。因此,最基本的事情是在浏览器中绘制一条3D直线。

自从WebGL问世以来,已经出现了一些JavaScript 3D库,它们抽象了实际WebGL库的内部细节,其中两个流行的JavaScript库是Three.jsBabylon.js。在本文中,我使用了Three.js库,并将展示关于如何在屏幕上绘制线条和曲面的代码片段。

Three.js中绘制3D对象所需的三个最重要的实体是SceneCameraRenderer。其代码如下所示

scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000 );
renderer = new THREE.WebGLRenderer({ antialias: true });

3A. 在Three.js中绘制直线

一条线段由其在空间中的两个端点(x1, y1, z1)(x2, y2, z2)确定。

Three.js中,有一个名为BufferGeometryGeometry对象用于绘制直线段。以前有一个Geometry对象,但在Three.js的第125版中已弃用,取而代之的是BufferGeometry对象。

此外,还需要指定一种材质。线条和曲面有不同的材质。

使用以下代码绘制直线段

            const material = new THREE.LineBasicMaterial({ color: 0xff00ff });
            const geometry = new THREE.BufferGeometry();
            const vertices = [];
            vertices.push(-0.75, -0.75, -0.75);
            vertices.push(0.75, 0.75, 0.75);
            geometry.setAttribute(
                "position",
                new THREE.Float32BufferAttribute(vertices, 3));
            let line = new THREE.Line(geometry, material);
            scene.add(line);
            render();

在上面的代码中,直线绘制在端点(-0.75, -0.75, -0.75)(0.75,0.75,0.75)之间,如下图中的洋红色线所示

5295155/00bLine.png

3B. 在Three.js中绘制简单曲面

曲面被定义为一组三角形。因此,要绘制的基本几何实体是一个三角形。3D中的三角形由其三个顶点(x1, y1, z1)(x2, y2, z2)(x3, y3, z3)指定。我们不能只绘制定义三角形的三条直线,因为那样会显示为线框图。

如果场景只包含一组线段,则无需额外光源来照亮场景。然而,如果场景中有曲面或3D对象(如立方体、球体等),则必须指定一个或多个光源。光源有不同类型,这些在Three.js文档中定义。向场景添加两个光源的代码如下所示

            scene.add(new THREE.HemisphereLight(0x606060, 0x404040));

            // White directional light at 0.65 intensity shining from the top.
            let directionalLight = new THREE.DirectionalLight(0xffffff, 0.65);
            scene.add(directionalLight);        

为了将三角形指定为曲面,我们需要定义三角形的法线。现在,一个三角形有两个法线,都指向相反的方向。如果材质指定为双面,那么三角形的两面都将显示,并根据定义的光源进行照明。然而,如果曲面指定为单面,那么光线没有照射到的一面可能不会被照亮。uv值用于定义纹理,将图像坐标映射到曲面上的点。

在场景上绘制三角曲面的代码如下所示

          function computeCoonsBicubicSurface() {
            setupFourPoints();
            surfacePoints.length = 0;
            let uVal, wVal;
          
            for (let j = 0; j <= noDivisions; ++j) {
              wVal = j * step;
          
              for (let i = 0; i <= noDivisions; ++i) {
                uVal = i * step;
                let pt = computePointOnSurface(uVal, wVal);
                surfacePoints.push(pt.xVal, pt.yVal, pt.zVal);
                uvArray.push(1.0 - wVal);
                uvArray.push(uVal);
              }
            }
            renderCoonsBicubicSurface();
            handleUWValue();
          }
          
          function renderCoonsBicubicSurface() {
            scene.remove(surfaceMesh);
            scene.remove(lineWire);
          
            let material = new THREE.MeshStandardMaterial({
              side: THREE.DoubleSide,
              color: 0x00ffff,
              emissive: 0x111111,
              dithering: true,
              flatShading: false,
              roughness: 1,
              metalness: 0.15,
              skinning: true,
            });
          
            let materialLine = new THREE.LineBasicMaterial({
              color: 0x00ffff,
            });
            
            let materialT = new THREE.MeshBasicMaterial({
              map: textureImage,
              side: THREE.DoubleSide,
            });
          
            let geometry = new THREE.BufferGeometry();
            const indices = [];
            indices.length = 0;

            for (let i = 0; i < noDivisions; i++) {
                for (let j = 0; j < noDivisions; j++) {
                    const a = i * (noDivisions + 1) + (j + 1);
                    const b = i * (noDivisions + 1) + j;
                    const c = (i + 1) * (noDivisions + 1) + j;
                    const d = (i + 1) * (noDivisions + 1) + (j + 1);

                    // generate two faces (triangles) per iteration

                    indices.push(a, b, d); // face one
                    indices.push(b, c, d); // face two
                }
            }

            geometry.setAttribute(
             "position",
                   new THREE.Float32BufferAttribute(surfacePoints, 3).onUpload(disposeArray)
            );
            const uvNumComponents = 2;
            geometry.setAttribute(
                "uv",
                new THREE.BufferAttribute(
                new Float32Array(uvArray),
                uvNumComponents
                ).onUpload(disposeArray)
            );
            geometry.setIndex(indices);
            geometry.computeVertexNormals();

            if (document.getElementById("wireframe").checked === true) {
            let surfaceWire = new THREE.WireframeGeometry(geometry);
            lineWire = new THREE.LineSegments(surfaceWire, materialLine);
            scene.add(lineWire);
            } else {
            if (textureCheck.checked === true) {
              materialT.map = new THREE.CanvasTexture(canvasBig);
              surfaceMesh = new THREE.Mesh(geometry, materialT);
            } else {
              surfaceMesh = new THREE.Mesh(geometry, material);
            }
            surfaceMesh.material.needsUpdate = true;
            scene.add(surfaceMesh);
            render();
          }        

其中surfacePoints是上面定义的点数组。使用上述代码绘制的曲面如下所示。随着三角形数量的增加,曲面开始呈现平滑的外观。请参阅文件script2.js以获取完整代码。

5295155/00aSurf.png

4. 3D参数三次曲线

参数三次曲线由以下三个方程定义

x(u) = B0x + B1x u + B2x u2 + B3x u3
y(u) = B0y + B1y u + B2y u2 + B3y u3
z(u) = B0z + B1z u + B2z u2 + B3z u3

这里,12个常数B0x, B1x, B2x, B3x, B0y, B1y, B2y, B3y, B0z, B1z, B2z, B3z是根据曲线上点确定的常数。

确定这些常数有两种方法

  1. 四点形式:如果已知曲线上有四个点,则这四个点的坐标可以指定十二个方程来确定十二个常数。
  2. Hermite形式:如果已知两个端点的坐标,以及两个端点的切线(或者说是端点处关于x, y, z的导数),那么这些也可以指定十二个方程来确定十二个常数。

这些是本文附带应用程序中介绍的两种方法。对应于这两种形式的方程推导可在Mortenson的《几何建模》一书或Rogers和Adams的《计算机图形学数学元素》一书中找到。对于Hermite形式,x, y, z方程相对于x, y, z坐标的导数,这些在上述书籍中也有提及。

4A. 四点形式

对于这种四点形式,我们需要为四个点指定u值。为了本代码的目的,我们将其取为0, 1/3, 2/3 和 1。对于这些参数u的值,将它们代入曲线方程,可以确定常数Bij的值。当通过坐标值的滚动条改变四个点中任何一个点的坐标值时,这些常数Bij会被动态计算,曲线上所有点的新坐标也会被计算,并且曲线会在屏幕上刷新。其代码如下:

          function computePointFourPointForm(uVal) {
            let u2, u3;
            let coeff1, coeff2, coeff3, coeff4;
            let xCurve, yCurve, zCurve;
          
            u2 = uVal * uVal;
            u3 = u2 * uVal;
          
            // This is the Four Point Formula from Mortenson's book on Geometric Modeling
            // For values of u being 0, 1/3, 2/3 and 1.
            coeff1 = -4.5 * u3 + 9 * u2 - 5.5 * uVal + 1;
            coeff2 = 13.5 * u3 - 22.5 * u2 + 9 * uVal;
            coeff3 = -13.5 * u3 + 18 * u2 - 4.5 * uVal;
            coeff4 = 4.5 * u3 - 4.5 * u2 + uVal;
            xCurve = p1x * coeff1 + p2x * coeff2 + p3x * coeff3 + p4x * coeff4;
            yCurve = p1y * coeff1 + p2y * coeff2 + p3y * coeff3 + p4y * coeff4;
            zCurve = p1z * coeff1 + p2z * coeff2 + p3z * coeff3 + p4z * coeff4;
            return {
              xVal: xCurve,
              yVal: yCurve,
              zVal: zCurve,
            };
          }        

4B. Hermite形式

对于这种形式,指定两个端点和这两个端点的两组导数,并用它们来计算常数Bij。这些也来自于Mortenson书中给出的公式。其代码如下:

          function computePointHermiteForm(uVal) {
            let u2, u3;
            let coeff1, coeff2, coeff3, coeff4;
            let xCurve, yCurve, zCurve;
          
            u2 = uVal * uVal;
            u3 = u2 * uVal;
          
            // This is the Hermite Formula from Mortenson's book on Geometric Modeling
            // u and du at the endpoints.
            coeff1 = 2 * u3 - 3 * u2 + 1;
            coeff2 = -2 * u3 + 3 * u2;
            coeff3 = u3 - 2 * u2 + uVal;
            coeff4 = u3 - u2;
            xCurve = p1xh * coeff1 + p2xh * coeff2 + p1dxh * coeff3 + p2dxh * coeff4;
            yCurve = p1yh * coeff1 + p2yh * coeff2 + p1dyh * coeff3 + p2dyh * coeff4;
            zCurve = p1zh * coeff1 + p2zh * coeff2 + p1dzh * coeff3 + p2dzh * coeff4;
            return {
              xVal: xCurve,
              yVal: yCurve,
              zVal: zCurve,
            };
          }        

当用户使用屏幕上的滚动条修改与端点坐标和导数对应的滚动条时,常数Bij会被动态计算,整个曲线也会重新计算。

4C. 参数三次曲线的代码

曲线以曲线上的一组直线段形式渲染。整个参数范围0 ≤ u ≤ 1被分成若干部分,所有直线段都在循环中绘制。其代码在文件script1.js中。对应的HTML在文件page1.html中。

            let curvePoints = []; 
            curvePoints.length = 0;
            
            for (let i = 0; i < noUPoints; ++i) {
                uVal = uStep * i; // uVal and uStep are defined earlier
                let pt = computePointFourPointForm(uVal);
                // let pt = computePointHermiteForm(uVal);
                curvePoints.push(pt.xVal, pt.yVal, pt.zVal);
            }        

4D. 验证

为了验证,使用一组标准的点坐标/导数值,并观察所得曲线的形状。此外,通过改变参数u获得的曲线上点在屏幕上显示,并用于验证。

  1. 这里需要注意一个有趣的案例,即**非线性直线**。通常情况下,参数u的相等增量不一定会沿着曲线产生相等的距离。这引入了参数三次曲线中的非线性元素。这是一条非线性直线。尽管所有点都位于同一条直线上,但不同的参数增量会导致沿着该直线的不同遍历距离。
  2. 在Hermite形式中,对于一条直线,端点导数可能指向直线段之外的方向。在这种情况下,当参数u0增加到1时,点会离开直线段,然后反转方向,然后朝向第二个端点。这又是一条**非线性直线。**
  3. 屏幕上以按钮形式提供了几个这样的案例,指定了有趣的曲线。

5. 3D贝塞尔曲线

上面所示的参数三次曲线,特别是在四点形式中,曲线通过所有四个点,这是一种曲线拟合形式。贝塞尔曲线由P Bezier引入,他根据控制点定义了曲线方程。贝塞尔曲线由以下方程定义

5295155/01BezCurveEqn.png

在我们的应用程序中,我们考虑了一个具有5个控制点的贝塞尔曲线,因此多项式中u的最高次数为4。

5A. 贝塞尔曲线的代码

与参数三次曲线的情况一样,贝塞尔曲线也以曲线上的一组直线段形式渲染。整个参数范围0 ≤ u ≤ 1被分成若干部分,所有直线段都在循环中绘制。其代码在文件script3.js中,该文件对应于HTML文件page3.html

            u2 = uVal * uVal;
            u3 = u2 * uVal;
            u4 = u3 * uVal;
          
            // This is the Bezier Curve Formula from Rogers and Adam's Book - Mathematical Elements for
            // Computer Graphics
            coeff1 = u4 - 4 * u3 + 6 * u2 - 4 * uVal + 1;
            coeff2 = -4 * u4 + 12 * u3 - 12 * u2 + 4 * uVal;
            coeff3 = 6 * u4 - 12 * u3 + 6 * u2;
            coeff4 = -4 * u4 + 4 * u3;
            coeff5 = u4;
            xCurve = p1x * coeff1 + p2x * coeff2 + p3x * coeff3 + p4x * coeff4 + p5x * coeff5;
            yCurve = p1y * coeff1 + p2y * coeff2 + p3y * coeff3 + p4y * coeff4 + p5y * coeff5;
            zCurve = p1z * coeff1 + p2z * coeff2 + p3z * coeff3 + p4z * coeff4 + p5z * coeff5;        

5B. 验证

贝塞尔曲线有以下验证方面

  1. 在一般情况下,曲线应通过端点,而不应通过其他控制点。
  2. 然而,当所有控制点都在一条直线上时,曲线也应该是通过这些点的直线。
  3. 曲线在端点处的切线应与该特定端点与其下一个控制点之间的连线对齐。例如,在曲线的起点,曲线的切线应与连接第一个和第二个控制点的直线对齐。终点控制点也类似。

所有这些点都已对贝塞尔曲线进行验证,并绘制了一组有趣的贝塞尔曲线,每个曲线在屏幕上都有一个自己的按钮。

6. 3D NURBS曲线

NURBS代表非均匀有理B样条曲线。这种曲线的必要性 arose 是因为参数三次曲线和贝塞尔曲线都不允许对曲线进行局部控制。换句话说,对于参数三次曲线和贝塞尔曲线,修改一个控制点会修改整个曲线,这在许多应用程序中是不希望的。另一方面,B样条曲线允许对曲线进行局部控制。修改控制点的坐标只会导致曲线在该控制点附近发生修改,而曲线的其余部分不受影响。

有理B样条曲线的一般方程如下

5295155/02NurbsCurveEqn.png

NURBS曲线的度数不依赖于控制点的数量。这两者在NURBS曲线中是独立的实体。

在我们的应用程序中,我们最初定义了六个控制点,并允许用户添加控制点(最多20个控制点),并修改每个控制点的坐标(x, y, z, h值),并可视化生成的NURBS曲线。

6A. NURBS曲线代码

由于Three.js已经有NURBS曲线的开源代码,我们不打算重复造轮子。因此,这里我们从那里提取了NURBS曲线代码的相关部分,并将其包含在一个名为NurbsHelper.js的文件中。

在我们的代码中,为了指定控制点,我们在以原点为中心、尺寸为2的边界框内生成随机数。

尽管在现实中,NURBS曲线是我们应用程序中三种曲线中最复杂的,但其代码——script5.js是最简单的,它只调用文件NurbsHelper.js中的相关函数。对应的HTML文件是page5.html

6B. 验证

  1. 如前所述,NURBS曲线允许局部控制,这意味着通过修改一个点的坐标(x, y, z, h),曲线只会在局部发生修改,而曲线的其他部分保持不变。这是一个可以很容易通过视觉验证的属性。
  2. 另一个需要验证的点是修改控制点齐次坐标h的值。这应该将曲线拉向该控制点。这也通过视觉进行了验证。
  3. 另一个验证点是修改曲线的度数。当度数等于1时,曲线类似于控制多边形本身。随着曲线度数的增加,曲线远离控制点(除了端点),对于更高的度数值,曲线离其对应的控制点最远。

7. Coons双三次曲面

参数三次曲线的二维等价物是Coons双三次曲面。这类曲面的方程如下

5295155/04coons.png

在参数空间中,有两个参数uw,它们在[0, 1]范围内变化。Coons双三次曲面需要定义以下边界条件

  1. 矩形片区的四个端点。
  2. 这些端点处相对于参数u的切线向量。它们是相对于u的偏导数。
  3. 这些端点处相对于参数w的切线向量。它们是相对于w的偏导数。
  4. 这些端点处相对于参数u, w的扭曲向量。它们是相对于uw的偏导数。

当上述任何一个发生变化时,曲面也会随之改变。

所有这些都显示在下图所示

5295155/03surface2.png

曲面的边界是这四条曲线

  1. u增加且w为0的曲线。
  2. w增加且u为0的曲线。
  3. u增加且w为1的曲线。
  4. w增加且u为1的曲线。

7A. Coons双三次曲面的代码

以下JavaScript函数在文件script2.js中计算曲面上的一个点。

          function computePointOnSurface(uVal, wVal) {
            let u2, u3, w2, w3;
            let f1u, f2u, f3u, f4u, f1w, f2w, f3w, f4w;
            let valueX, valueY, valueZ;
            let valx1, valx2, valx3, valx4;
            let valy1, valy2, valy3, valy4;
            let valz1, valz2, valz3, valz4;
          
            w2 = wVal * wVal;
            w3 = w2 * wVal;
            f1w = 2.0 * w3 - 3 * w2 + 1.0;
            f2w = -2.0 * w3 + 3.0 * w2;
            f3w = w3 - 2.0 * w2 + wVal;
            f4w = w3 - w2;
            u2 = uVal * uVal;
            u3 = u2 * uVal;
            f1u = 2.0 * u3 - 3 * u2 + 1.0;
            f2u = -2.0 * u3 + 3.0 * u2;
            f3u = u3 - 2.0 * u2 + uVal;
            f4u = u3 - u2;
          
            valx1 = f1u * (p1x * f1w + p2x * f2w + p1wx * f3w + p2wx * f4w);
            valx2 = f2u * (p3x * f1w + p4x * f2w + p3wx * f3w + p4wx * f4w);
            valx3 = f3u * (p1ux * f1w + p2ux * f2w + p1uwx * f3w + p2uwx * f4w);
            valx4 = f4u * (p3ux * f1w + p4ux * f2w + p3uwx * f3w + p4uwx * f4w);
            valueX = valx1 + valx2 + valx3 + valx4;
          
            valy1 = f1u * (p1y * f1w + p2y * f2w + p1wy * f3w + p2wy * f4w);
            valy2 = f2u * (p3y * f1w + p4y * f2w + p3wy * f3w + p4wy * f4w);
            valy3 = f3u * (p1uy * f1w + p2uy * f2w + p1uwy * f3w + p2uwy * f4w);
            valy4 = f4u * (p3uy * f1w + p4uy * f2w + p3uwy * f3w + p4uwy * f4w);
            valueY = valy1 + valy2 + valy3 + valy4;
          
            valz1 = f1u * (p1z * f1w + p2z * f2w + p1wz * f3w + p2wz * f4w);
            valz2 = f2u * (p3z * f1w + p4z * f2w + p3wz * f3w + p4wz * f4w);
            valz3 = f3u * (p1uz * f1w + p2uz * f2w + p1uwz * f3w + p2uwz * f4w);
            valz4 = f4u * (p3uz * f1w + p4uz * f2w + p3uwz * f3w + p4uwz * f4w);
            valueZ = valz1 + valz2 + valz3 + valz4;
          
            return {
              xVal: valueX,
              yVal: valueY,
              zVal: valueZ,
            };
          }        

7B. 验证

以下是验证方面

  1. 当使用滑块改变边界点的坐标时,相应的边界点应在指定的x、yz方向上发生变化,并且曲面也应相应地改变。
  2. 当使用滑块改变uv方向上的切线向量时,也应发生同样的情况,尽管这种变化不如改变坐标值那样明显。
  3. 类似地,当扭曲向量改变时,曲面也应该改变。
  4. 还呈现了一些预定义曲面,点击左侧窗格底部的按钮即可出现。

8. 贝塞尔曲面

贝塞尔曲线的二维(在参数空间中)等价物是贝塞尔曲面。这类曲面的方程如下

5295155/05BezSurf.png

8A. 贝塞尔曲面代码

以下JavaScript函数在文件script4.js中计算贝塞尔曲面上的一个点。为了本文的目的,提供的代码是针对4 x 4贝塞尔曲面的,总共有16个控制点。对应的HTML文件是page4.html

          function computeBezierSurfacePoint(uVal, wVal) {
            let u2, u3, w2, w3;
            u2 = uVal * uVal;
            u3 = uVal * u2;
            w2 = wVal * wVal;
            w3 = wVal * w2;
          
            // Need to note the following regarding THREE.js Matrix4.
            // When we set the matrix, we set it in row major order.
            // However, when we access the elements of this matrix, these are
            // returned in column major order.
            let matC = new THREE.Matrix4();
            matC.set(-1, 3, -3, 1, 3, -6, 3, 0, -3, 3, 0, 0, 1, 0, 0, 0);
          
            let matPx = new THREE.Matrix4();
            matPx.set(
              p00x, p10x, p20x, p30x, p01x, p11x, p21x, p31x,
              p02x, p12x, p22x, p32x, p03x, p13x, p23x, p33x
            );
          
            let matPy = new THREE.Matrix4();
            matPy.set(
              p00y, p10y, p20y, p30y, p01y, p11y, p21y, p31y,
              p02y, p12y, p22y, p32y, p03y, p13y, p23y, p33y
            );
          
            let matPz = new THREE.Matrix4();
            matPz.set(
              p00z, p10z, p20z, p30z, p01z, p11z, p21z, p31z,
              p02z, p12z, p22z, p32z, p03z, p13z, p23z, p33z
            );
          
            let mat1x = new THREE.Matrix4();
            mat1x.multiplyMatrices(matC, matPx);
          
            let mat1y = new THREE.Matrix4();
            mat1y.multiplyMatrices(matC, matPy);
          
            let mat1z = new THREE.Matrix4();
            mat1z.multiplyMatrices(matC, matPz);
          
            let mat2x = new THREE.Matrix4();
            mat2x.multiplyMatrices(mat1x, matC);
          
            let mat2y = new THREE.Matrix4();
            mat2y.multiplyMatrices(mat1y, matC);
          
            let mat2z = new THREE.Matrix4();
            mat2z.multiplyMatrices(mat1z, matC);
          
            // We access the matrix elements in column major order.
            let ex = mat2x.elements;
            let w0x = ex[0] * w3 + ex[4] * w2 + ex[8] * wVal + ex[12];
            let w1x = ex[1] * w3 + ex[5] * w2 + ex[9] * wVal + ex[13];
            let w2x = ex[2] * w3 + ex[6] * w2 + ex[10] * wVal + ex[14];
            let w3x = ex[3] * w3 + ex[7] * w2 + ex[11] * wVal + ex[15];
          
            let ey = mat2y.elements;
            let w0y = ey[0] * w3 + ey[4] * w2 + ey[8] * wVal + ey[12];
            let w1y = ey[1] * w3 + ey[5] * w2 + ey[9] * wVal + ey[13];
            let w2y = ey[2] * w3 + ey[6] * w2 + ey[10] * wVal + ey[14];
            let w3y = ey[3] * w3 + ey[7] * w2 + ey[11] * wVal + ey[15];
          
            let ez = mat2z.elements;
            let w0z = ez[0] * w3 + ez[4] * w2 + ez[8] * wVal + ez[12];
            let w1z = ez[1] * w3 + ez[5] * w2 + ez[9] * wVal + ez[13];
            let w2z = ez[2] * w3 + ez[6] * w2 + ez[10] * wVal + ez[14];
            let w3z = ez[3] * w3 + ez[7] * w2 + ez[11] * wVal + ez[15];
          
            let qx = u3 * w0x + u2 * w1x + uVal * w2x + w3x;
            let qy = u3 * w0y + u2 * w1y + uVal * w2y + w3y;
            let qz = u3 * w0z + u2 * w1z + uVal * w2z + w3z;
          
            return {
              xVal: qx,
              yVal: qy,
              zVal: qz,
            };
          }        

8B. 验证

  1. 这里再次,当任何控制点发生改变(坐标)时,相应的曲面也应该改变。这通过观察渲染的曲面来验证。
  2. 通过点击按钮,可以显示一些有趣的曲面。

9. NURBS曲面

NURBS曲线的二维(在参数空间中)等价物是NURBS曲面。NURBS曲面优于其他两种类型曲面的优点是,NURBS曲面提供了曲面的局部控制。修改控制点的坐标只会影响该控制点附近的曲面,而曲面的其余部分不受影响。这是因为NURBS曲面对应的基函数。

9A. NURBS曲面代码

同样地,Three.js库的开发者提供了一个JavaScript文件来计算NURBS曲面上点的坐标,我们从中提取了相关部分并将其用于此应用程序。这些内容在文件NurbsHelper.js中。与NURBS曲线的情况一样,定义了在uw方向上的两个节点矢量。

在本应用程序中,我们定义了一个NURBS曲面,在uw方向上各有7个控制点。因此,用户总共可以修改49个控制点。对于每个控制点,用户都可以修改其x、y、z、h值在一定范围内。

NURBS曲面计算的代码在文件script6.js中,内容如下。HTML是page6.html

          function computeNurbsSurface() {
            nurbsSurface = new NURBSSurface(
              degreeU,
              degreeW,
              knotVectorU,
              knotVectorW,
              points
            );
          
            surfacePoints.length = 0;
            let uVal, wVal;
          
            for (let j = 0; j <= noDivisions; ++j) {
              wVal = j * step;
              for (let i = 0; i <= noDivisions; ++i) {
                uVal = i * step;
                let pt = new Vector3();
                nurbsSurface.getPoint(uVal, wVal, pt);
                //let poi = new THREE.Vector3();
                surfacePoints.push(pt.x, pt.y, pt.z);
              }
            }
            renderNurbsSurface();
          }        

9B. 验证

  1. NURBS曲面应穿过整个面片的角点。
  2. 当控制点的坐标改变时,曲面应仅在局部发生改变。全局上,曲面应不受影响。

10. 关注点

  • 我的目的是让每种类型的曲线/曲面都在其自己的文件夹中,包含自己的HTML和JS文件。这样做的目的是让它们的逻辑彼此分离。它们都引用同一个three.min.js文件,这是一个WebGL库文件。此外,它们都引用同一个style.css文件。
  • 因此,安排是这样的:有六个不同的文件夹,p01CubicCurvep02CubicSurfacep03BezierCurvep04BezierSurfacep05NurbsCurvep06NurbsSurface。每个文件夹都有其HTML和JS文件。
  • 此外,还有一个名为js的文件夹,其中包含三个文件:three.min.jsscript.jsNurbsHelper.js
  • 有了上述代码安排,可能会出现一些代码重复。例如,绘制边界框的代码部分在六个JS文件(script1.jsscript2.jsscript3.jsscript4.jsscript5.jsscript6.js)中重复了六次。我故意这样保留,因为任何想要学习这些独立文件的学习者现在都可以从它们各自的文件夹中取出它们,并直接在他们的应用程序中使用,而无需费心整合代码。这些文件夹之外唯一需要获取的代码将是three.min.js缩小版库文件和CSS文件。
  • 您可能会注意到,应用程序中没有文本框类型的输入。用户交互只通过滑块、复选框、组合框和按钮进行。这是我防止软件出错的方法。您唯一可能让应用程序行为异常的方式是伪装非图像文件为图像文件,那样它就无法工作。我希望您不会“崩溃”这个应用程序。如果您遇到应用程序崩溃、屏幕变空白或出现其他异常行为的情况,请随时通过下面的评论区通知我。
  • Three.js的(版本125)中,几何体的处理方式发生了重大改变。他们移除了THREE.Geometry并引入了THREE.BufferGeometry。由于我不希望Three.js库的任何进一步修改影响我代码的行为,我已将此库的压缩版本包含在代码中。这样,代码是自包含的,可以在没有互联网连接的情况下,通过本地服务器运行。
  • 应用程序本身具有简单直观的用户界面,没有任何花哨。有一个菜单,可以选择所需的曲线和曲面类型。选择菜单项后,相应的屏幕会出现在菜单下方。左侧面板是滑块和其他控件,它们修改右侧HTML Canvas元素上的3D对象。用户不能直接与Canvas元素交互,只能通过左侧的控件进行交互。
  • 这里描述的数学并不是新颖的,它已经有四十多年的历史了。有许多软件包应用这种数学来创建几何模型。然而,这里以一种自包含的方式,以完全客户端JavaScript代码运行的形式呈现的封装似乎是新颖的。

11. 画廊

下面是一些可以使用此应用程序设计的曲面画廊

5295155/gallery2.png

12. 总结

在本文中,我们描述并演示了使用Three.js库在浏览器中绘制这六种几何对象的代码——参数三次曲线、贝塞尔曲线、NURBS曲线、Coons双三次曲面、贝塞尔曲面和NURBS曲面。该应用程序允许您修改单个控制点的坐标(在范围内),在某些情况下还可以修改切线(导数)的方向。用户还可以以线框模式查看曲面,并改变摄像机角度,从不同方向查看曲线或曲面。此外,还增加了一个功能,可以输入图像文件(PNG、JPG)并将其作为纹理渲染到曲面上。所有这些都用纯原生JavaScript编写,我已在Chrome、Safari和Edge浏览器上进行了测试。

我非常享受编写这段代码的每一个过程,尤其是当滑块移动时,屏幕上的3D物体首次移动的时刻,令人激动。我希望您会喜欢使用这个应用程序,并观察当控制点通过滑块改变时,这些曲线和曲面动态变化。如果您在应用程序中发现任何异常行为,请在下面的评论区写信给我。即使没有异常,也请留下您的评论。

您也可以在https://github.com/amarnaths0005/3DCurvesSurfaces下载代码。

您可以在https://amarnaths0005.github.io/3DCurvesSurfaces/#two上运行该应用程序。

致谢

我非常感谢印度科学学院班加罗尔分校机械工程系的教授,在我还是学生时教授了这门课程。

历史

  • 2021年2月18日:版本1.0
  • 2021年8月18日:版本1.1 - 修复了曲面法线计算问题。
  • 2023年2月20日:版本1.2 - 添加了纹理功能,可以将图像用作曲面纹理。
© . All rights reserved.