I am trying to calculate the normals of triangles using TypeScript, but I am getting unexpected results. Here are the details of the vertices and triangles used:
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
];
I have also applied some rotations and translations to the vertices, which is working correctly. However, when I try to calculate the normals of the triangles using the calculateNormal()
function, the results appear to be incorrect.
Here is the implementation of the rotations and translation:
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);
Here is the implementation of the calculateNormal()
function:
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;
}
Here is the drawWireframe function:
/**
* 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";
}
};
I have verified that the calculateNormal()
function is being called correctly for each triangle, but the resulting normals do not seem to be correct.
Here an example of the cube (The red lines in the drawing represent the direction of the normals): cube
I would appreciate any advice or suggestions on what I might be doing wrong and how to fix it.
I fixed the problem by changing the triangles' vertex order and multiplying the normal.x by -1.
Vertex change:
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 }
];
Using this ordination, all the triangles are pointing outwards, making all the normals also point out.
Multiply by -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;
}
For some reason that I didn't get, when I multiply the x by -1, the normals are drawn constantly parallel and right. Removing that multiplication doesn't seem to affect the backface culling since the validation uses only the z parameter.