TypeScript 3D引擎三角形法线计算异常求助
3D引擎三角形法线计算异常问题排查
问题背景
使用TypeScript开发3D引擎时,计算立方体三角形法线的结果不符合预期。已确认顶点旋转、平移操作正确,但绘制出的法线方向混乱(部分法线指向立方体内部或朝向错误)。
顶点与三角形数据
interface Vertex { x: number; y: number; z: number; } interface Triangle { v1: number; v2: number; v3: number; } 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 ];
核心实现代码
顶点变换与法线计算逻辑
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);
法线计算函数
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; }
线框绘制函数
/** * 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]; // 绘制三角形 ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.lineTo(p3.x, p3.y); ctx.closePath(); ctx.stroke(); // 计算三角形中心点 const centerX = (p1.x + p2.x + p3.x) / 3; const centerY = (p1.y + p2.y + p3.y) / 3; // 获取对应法线并缩放 const normal = normals[i]; const endX = centerX + normal.x * normalScale; const endY = centerY + normal.y * normalScale; // 绘制法线 ctx.strokeStyle = "#ff0000"; ctx.beginPath(); ctx.moveTo(centerX, centerY); ctx.lineTo(endX, endY); ctx.stroke(); ctx.strokeStyle = "#ffffff"; } };
问题分析与修复建议
1. 三角形顶点缠绕顺序不一致
法线方向由顶点的顺时针/逆时针顺序(右手定则)决定。当前部分面的顶点顺序混乱:
- 正面顶点为逆时针排列,法线向外;但背面顶点为顺时针排列,导致法线指向内部。
修复: 统一所有三角形的顶点缠绕顺序(全部使用逆时针),修改背面三角形定义:
// 原背面三角形 // { v1: 4, v2: 5, v3: 6 }, { v1: 4, v2: 6, v3: 7 }, // 修改为 { v1: 4, v2: 6, v3: 5 }, { v1: 4, v2: 7, v3: 6 },
同步检查其他面的顶点顺序,确保从外部观察均为逆时针排列。
2. Canvas坐标系Y轴方向差异
3D空间Y轴通常向上,而Canvas的Y轴向下,导致法线绘制时Y方向反转。
修复: 绘制法线时反转Y分量:
// 原代码 // const endY = centerY + normal.y * normalScale; // 修改为 const endY = centerY - normal.y * normalScale;
3. 背面剔除的空间坐标系不匹配
当前基于世界空间的normal.z > 0判断背面,若相机朝向-Z方向,视图空间的Z轴逻辑会与世界空间冲突。
修复: 将法线转换到视图空间后再做剔除判断:
// 假设viewMatrix为相机视图矩阵 const viewNormal = multiplyMatrixVector(viewMatrix, [normal.x, normal.y, normal.z, 0]); if (viewNormal.z > 0) { culledTriangles.push(triangles[i]); normals.push(normal); }
注意:变换法线时使用向量(w=0),避免平移操作影响方向。
内容的提问来源于stack exchange,提问作者RalkB
相关产品推荐
相关产品推荐

