在 TypeScript 中为 3D 引擎计算三角形法线时出错

Error calculating triangle normals in TypeScript for a 3d engine

提问人:RalkB 提问时间:4/29/2023 最后编辑:RalkB 更新时间:5/2/2023 访问量:56

问:

我正在尝试使用TypeScript计算三角形的法线,但我得到了意想不到的结果。以下是所用顶点和三角形的详细信息:

let vertices: Vertex[] = [
  { x: 0, y: 0, z: 0 },
  { x: 1, y: 0, z: 0 },
  { x: 1, y: 1, z: 0 },
  { x: 0, y: 1, z: 0 },
  { x: 0, y: 0, z: 1 },
  { x: 1, y: 0, z: 1 },
  { x: 1, y: 1, z: 1 },
  { x: 0, y: 1, z: 1 },
];

const triangles: Triangle[] = [
  { v1: 0, v2: 1, v3: 2 }, { v1: 0, v2: 2, v3: 3 }, // Front face
  { v1: 4, v2: 5, v3: 6 }, { v1: 4, v2: 6, v3: 7 }, // Back face
  { v1: 0, v2: 1, v3: 5 }, { v1: 0, v2: 5, v3: 4 }, // Bottom face
  { v1: 2, v2: 3, v3: 7 }, { v1: 2, v2: 7, v3: 6 }, // Top face
  { v1: 0, v2: 3, v3: 7 }, { v1: 0, v2: 7, v3: 4 }, // Left face
  { v1: 1, v2: 2, v3: 6 }, { v1: 1, v2: 6, v3: 5 }  // Right face
];

我还对顶点应用了一些旋转和平移,这些旋转和平移工作正常。但是,当我尝试使用该函数计算三角形的法线时,结果似乎不正确。calculateNormal()

以下是轮换和平移的实现:

const angle = 1;
const rotationX = rotationMatrixX(angle);
const rotationY = rotationMatrixY(angle); 
vertices = vertices.map((vertex) => {
    let [x, y, z, w] = multiplyMatrixVector(rotationX, [
        vertex.x,
        vertex.y,
        vertex.z,
        1,
    ]);
    return {x, y, z};
})
vertices = vertices.map((vertex) => {
    let [x, y, z, w] = multiplyMatrixVector(rotationY, [
        vertex.x,
        vertex.y,
        vertex.z,
        1,
    ]);
    return {x, y, z};
})
const translatedVertices = transformVertices(vertices, translationMatrix);
const culledTriangles = [];
const normals = [];
for (let i = 0; i < triangles.length; i++) {
    const { v1, v2, v3 } = triangles[i];
    const p1 = translatedVertices[v1];
    const p2 = translatedVertices[v2];
    const p3 = translatedVertices[v3];

    const normal = calculateNormal(p1, p2, p3);

    if (normal.z > 0) {
        culledTriangles.push(triangles[i]);
        normals.push(normal);
    }
}
// Transform and project the rotated vertices
const transformedVertices = transformVertices(translatedVertices, perspectiveProjectionMatrix());
const scaledVertices = scaleAndNormalizeVertices(transformedVertices);

// Draw the rotated cube
drawWireframe(ctx, scaledVertices, culledTriangles, normals);


以下是该函数的实现:calculateNormal()

function calculateNormal(a: Vertex, b: Vertex, c: Vertex): Vertex {
  const line1: Vertex = {
    x: b.x - a.x,
    y: b.y - a.y,
    z: b.z - a.z,
  };

  const line2: Vertex = {
    x: c.x - a.x,
    y: c.y - a.y,
    z: c.z - a.z,
  };

  const normal: Vertex = {
    x: line1.y * line2.z - line1.z * line2.y,
    y: line1.z * line2.x - line1.x * line2.z,
    z: line1.x * line2.y - line1.y * line2.x,
  };

  const length = Math.sqrt(normal.x ** 2 + normal.y ** 2 + normal.z ** 2);
  normal.x /= length;
  normal.y /= length;
  normal.z /= length;

  return normal;
}

下面是 drawWireframe 函数:

/**
 * Draw the wireframe of a 3D object
 * @param ctx - The 2D rendering context of the canvas.
 * @param vertices - The array of vertices of the 3D object.
 * @param triangles - The array of triangles of the 3D object.
 */
export function drawWireframe(ctx: CanvasRenderingContext2D, vertices: Vertex[], triangles: Triangle[], normals: Vertex[]) {
    ctx.fillStyle = "#000000";
    ctx.fillRect(0, 0, config.canvas.width, config.canvas.height);

    ctx.strokeStyle = "#ffffff";

    // Add a scale factor to normals to make them more visible
    const normalScale = 50;

    for (let i = 0; i < triangles.length; i++) {
        const { v1, v2, v3 } = triangles[i];
        const p1 = vertices[v1];
        const p2 = vertices[v2];
        const p3 = vertices[v3];

        // Desenhar o triângulo
        ctx.beginPath();
        ctx.moveTo(p1.x, p1.y);
        ctx.lineTo(p2.x, p2.y);
        ctx.lineTo(p3.x, p3.y);
        ctx.closePath();
        ctx.stroke();

        // Calculate the center point of the triangle
        const centerX = (p1.x + p2.x + p3.x) / 3;
        const centerY = (p1.y + p2.y + p3.y) / 3;

        // Obtain the corresponding normal and scale

        const normal = normals[i];
        const endX = centerX + normal.x * normalScale;
        const endY = centerY + normal.y * normalScale;

        // draw the normal
        ctx.strokeStyle = "#ff0000";
        ctx.beginPath();
        ctx.moveTo(centerX, centerY);
        ctx.lineTo(endX, endY);
        ctx.stroke();
        ctx.strokeStyle = "#ffffff";
    }
};

我已经验证了每个三角形的函数是否被正确调用,但生成的法线似乎不正确。calculateNormal()

下面是立方体的示例(图中的红线表示法线的方向):立方体

我将不胜感激任何关于我可能做错了什么以及如何解决它的建议或建议。

数学 图形 3D 游戏引擎 法线

评论

0赞 kikon 4/29/2023
好吧,在您发布的代码中一切看起来都很好......但缺少很多东西。我很好奇你在图像中放置了法线的什么位置,为什么它们与边缘相比显得如此短,因为它们的长度肯定是 1。在任何情况下,建议使用最小的可重现示例,以便我们可以查看(并测试)错误可能来自的每个地方。
0赞 RalkB 4/30/2023
@kikon 感谢您的回答,我将寻找如何制作一个最小的可重现示例,我还通过添加三角形渲染实现细节来编辑我的问题。

答:

0赞 RalkB 5/2/2023 #1

我通过更改三角形的顶点顺序并将 normal.x 乘以 -1 解决了这个问题。

顶点变化:

const triangles: Triangle[] = [
    { v1: 0, v2: 2, v3: 1 }, { v1: 0, v2: 3, v3: 2 },
    { v1: 4, v2: 5, v3: 6 }, { v1: 4, v2: 6, v3: 7 },
    { v1: 0, v2: 1, v3: 5 }, { v1: 0, v2: 5, v3: 4 },
    { v1: 2, v2: 3, v3: 7 }, { v1: 2, v2: 7, v3: 6 },
    { v1: 0, v2: 7, v3: 3 }, { v1: 0, v2: 4, v3: 7 },
    { v1: 1, v2: 2, v3: 6 }, { v1: 1, v2: 6, v3: 5 }
];

使用这种排序,所有三角形都指向外,使所有法线也指向。

乘以 -1:

function calculateNormal(a: Vertex, b: Vertex, c: Vertex): Vertex {
    const line1: Vertex = {
        x: b.x - a.x,
        y: b.y - a.y,
        z: b.z - a.z,
    };

    const line2: Vertex = {
        x: c.x - a.x,
        y: c.y - a.y,
        z: c.z - a.z,
    };

    const normal: Vertex = {
        x: line1.y * line2.z - line1.z * line2.y,
        y: line1.z * line2.x - line1.x * line2.z,
        z: line1.x * line2.y - line1.y * line2.x,
    };

    const length = Math.sqrt(normal.x ** 2 + normal.y ** 2 + normal.z ** 2);
    normal.x /= length;
    normal.y /= length;
    normal.z /= length;
    normal.x *= -1; // multiply by -1
    return normal;
}

由于我没有得到的某种原因,当我将 x 乘以 -1 时,法线会不断平行且正确地绘制。删除该乘法似乎不会影响背面剔除,因为验证仅使用 z 参数。