计算机图形学中的齐次坐标:从基础到应用
一、齐次坐标的基本概念
在计算机图形学中,齐次坐标是一种用 N+1 维向量表示 N 维空间点的方法。在二维空间中,我们通常用 (x, y) 表示一个点,但在齐次坐标中,这个点被表示为 (x, y, w)。其中,w 是一个额外的坐标分量,当 w ≠ 0 时,对应的二维笛卡尔坐标为 (x/w, y/w)。
齐次坐标的优势在于它能够统一处理点和向量,并且能够简洁地表示平移、旋转、缩放等变换。
为什么需要齐次坐标?
在二维空间中,我们可以用矩阵乘法来表示旋转和缩放变换,但平移变换无法直接用 2×2 矩阵表示。齐次坐标通过增加一个维度,让我们能够用 3×3 矩阵统一表示所有的仿射变换(平移、旋转、缩放、剪切等)。
二、齐次坐标的表示
在齐次坐标中:
- 二维点表示为 (x, y, 1)
- 二维向量表示为 (x, y, 0)
- 当 w ≠ 1 时,点 (x, y, w) 对应的笛卡尔坐标为 (x/w, y/w)
向量的 w 分量为 0,这意味着向量在平移变换下不会改变,而点会受到平移的影响。
三、齐次坐标的变换矩阵
在齐次坐标系统中,二维变换可以用 3×3 矩阵表示。下面我们用 JavaScript 实现一个简单的齐次坐标和变换矩阵库。
JavaScript 实现
下面是一个基于齐次坐标的二维变换库的实现,包含了点和向量的表示以及基本变换矩阵的创建和应用。
class Vector3 {
constructor(x, y, w) {
this.x = x;
this.y = y;
this.w = w !== undefined ? w : 1; // 默认 w 为 1,表示点
}
// 转换为笛卡尔坐标
toCartesian() {
if (this.w === 0) {
return new Vector3(this.x, this.y, 0); // 向量保持不变
}
return new Vector3(this.x / this.w, this.y / this.w, 1);
}
// 应用变换矩阵
applyMatrix(matrix) {
const { x, y, w } = this;
const m = matrix.values;
return new Vector3(
m[0][0] * x + m[0][1] * y + m[0][2] * w,
m[1][0] * x + m[1][1] * y + m[1][2] * w,
m[2][0] * x + m[2][1] * y + m[2][2] * w
);
}
// 向量加法(仅用于 w=0 的向量)
add(v) {
if (this.w !== 0 || v.w !== 0) {
throw new Error('Vector addition is only defined for vectors (w=0)');
}
return new Vector3(this.x + v.x, this.y + v.y, 0);
}
// 向量点积(仅用于 w=0 的向量)
dot(v) {
if (this.w !== 0 || v.w !== 0) {
throw new Error('Dot product is only defined for vectors (w=0)');
}
return this.x * v.x + this.y * v.y;
}
// 向量长度(仅用于 w=0 的向量)
length() {
if (this.w !== 0) {
throw new Error('Length is only defined for vectors (w=0)');
}
return Math.sqrt(this.dot(this));
}
}
class Matrix3 {
constructor() {
// 初始化为单位矩阵
this.values = [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]
];
}
// 设置为平移矩阵
setTranslation(tx, ty) {
this.values = [
[1, 0, tx],
[0, 1, ty],
[0, 0, 1]
];
return this;
}
// 设置为旋转变换矩阵
setRotation(angleInRadians) {
const c = Math.cos(angleInRadians);
const s = Math.sin(angleInRadians);
this.values = [
[c, -s, 0],
[s, c, 0],
[0, 0, 1]
];
return this;
}
// 设置为缩放变换矩阵
setScaling(sx, sy) {
this.values = [
[sx, 0, 0],
[0, sy, 0],
[0, 0, 1]
];
return this;
}
// 矩阵乘法
multiply(matrix) {
const result = new Matrix3();
const a = this.values;
const b = matrix.values;
const c = result.values;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
}
}
return result;
}
// 应用于点或向量
transform(point) {
return point.applyMatrix(this);
}
}
四、基本变换的实现
1. 平移变换
平移变换将点 (x, y) 移动到 (x + tx, y + ty)。在齐次坐标中,平移矩阵为:
// 创建平移矩阵
const translationMatrix = new Matrix3();
translationMatrix.setTranslation(50, 100); // 沿 x 轴平移 50,沿 y 轴平移 100
// 创建点 (10, 20)
const point = new Vector3(10, 20);
// 应用平移变换
const translatedPoint = translationMatrix.transform(point);
console.log(translatedPoint.toCartesian()); // 输出: (60, 120, 1)
2. 旋转变换
旋转变换将点绕原点旋转一定角度。在齐次坐标中,旋转矩阵为:
// 创建旋转变换矩阵(绕原点旋转 90 度)
const rotationMatrix = new Matrix3();
rotationMatrix.setRotation(Math.PI / 2); // 90 度 = π/2 弧度
// 创建点 (10, 0)
const point = new Vector3(10, 0);
// 应用旋转变换
const rotatedPoint = rotationMatrix.transform(point);
console.log(rotatedPoint.toCartesian()); // 输出: (0, 10, 1)
3. 缩放变换
缩放变换改变点的大小。在齐次坐标中,缩放矩阵为:
// 创建缩放变换矩阵(x 方向缩放 2 倍,y 方向缩放 0.5 倍)
const scalingMatrix = new Matrix3();
scalingMatrix.setScaling(2, 0.5);
// 创建点 (10, 20)
const point = new Vector3(10, 20);
// 应用缩放变换
const scaledPoint = scalingMatrix.transform(point);
console.log(scaledPoint.toCartesian()); // 输出: (20, 10, 1)
五、变换的组合
在计算机图形学中,我们经常需要组合多个变换。例如,先旋转,再平移,最后缩放。变换的组合顺序非常重要,因为矩阵乘法不满足交换律。
变换顺序示例
下面的例子展示了不同变换顺序的效果:
// 创建一个点
const point = new Vector3(10, 0);
// 创建变换矩阵
const translationMatrix = new Matrix3().setTranslation(50, 0);
const rotationMatrix = new Matrix3().setRotation(Math.PI / 2);
// 顺序 1: 先旋转后平移
const transform1 = translationMatrix.multiply(rotationMatrix);
const result1 = transform1.transform(point);
console.log("先旋转后平移:", result1.toCartesian()); // 输出: (50, 10, 1)
// 顺序 2: 先平移后旋转
const transform2 = rotationMatrix.multiply(translationMatrix);
const result2 = transform2.transform(point);
console.log("先平移后旋转:", result2.toCartesian()); // 输出: (-10, 60, 1)
六、齐次坐标在透视投影中的应用
齐次坐标在透视投影中也有重要应用。透视投影是模拟人眼视觉的一种投影方式,远处的物体看起来比近处的小。
在透视投影中,我们可以通过修改齐次坐标的 w 分量来实现这种效果。例如,将点 (x, y, z) 投影到 z=0 的平面上:
// 透视投影矩阵
function createPerspectiveMatrix(d) {
const matrix = new Matrix3();
matrix.values = [ [1, 0, 0],
[0, 1, 0],
[0, 0, 1/d]
];
return matrix;
}
// 创建透视投影矩阵(d 是投影平面到视点的距离)
const perspectiveMatrix = createPerspectiveMatrix(100);
// 创建三维点 (50, 0, 50) 在齐次坐标中表示为 (50, 0, 50, 1)
// 注意:我们的 Vector3 类可以处理这种情况,w 分量默认为 1
const point3D = new Vector3(50, 0, 50);
// 应用透视投影
const projectedPoint = perspectiveMatrix.transform(point3D);
console.log("投影后的点:", projectedPoint.toCartesian()); // 输出: (100, 0, 1)
七、应用实例:实现一个简单的图形变换工具
下面我们用 HTML5 Canvas 和上面实现的齐次坐标库来创建一个简单的图形变换工具,支持平移、旋转和缩放。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>齐次坐标图形变换示例</title>
<style>
canvas {
border: 1px solid #ccc;
}
.controls {
margin-top: 10px;
}
</style>
</head>
<body>
<canvas id="canvas" width="500" height="400"></canvas>
<div class="controls">
<button id="translateBtn">平移</button>
<button id="rotateBtn">旋转</button>
<button id="scaleBtn">缩放</button>
<button id="resetBtn">重置</button>
</div>
<script>
// 前面定义的 Vector3 和 Matrix3 类的代码放在这里
// 获取 canvas 和绘图上下文
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 创建一个简单的图形(三角形)
const originalPoints = [
new Vector3(100, 50),
new Vector3(200, 150),
new Vector3(50, 150)
];
// 当前变换矩阵
let transformMatrix = new Matrix3();
// 绘制图形
function draw() {
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 应用变换
const transformedPoints = originalPoints.map(point =>
transformMatrix.transform(point).toCartesian()
);
// 绘制坐标系
ctx.strokeStyle = '#ccc';
ctx.beginPath();
ctx.moveTo(0, canvas.height / 2);
ctx.lineTo(canvas.width, canvas.height / 2);
ctx.moveTo(canvas.width / 2, 0);
ctx.lineTo(canvas.width / 2, canvas.height);
ctx.stroke();
// 绘制变换后的图形
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
ctx.beginPath();
ctx.moveTo(transformedPoints[0].x, transformedPoints[0].y);
for (let i = 1; i < transformedPoints.length; i++) {
ctx.lineTo(transformedPoints[i].x, transformedPoints[i].y);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
// 绘制变换后的点
ctx.fillStyle = 'blue';
transformedPoints.forEach(point => {
ctx.beginPath();
ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
ctx.fill();
});
}
// 初始化绘制
draw();
// 添加按钮事件
document.getElementById('translateBtn').addEventListener('click', () => {
// 创建平移矩阵并与当前矩阵相乘
const translateMatrix = new Matrix3().setTranslation(50, 30);
transformMatrix = translateMatrix.multiply(transformMatrix);
draw();
});
document.getElementById('rotateBtn').addEventListener('click', () => {
// 创建旋转变换矩阵并与当前矩阵相乘
const rotateMatrix = new Matrix3().setRotation(Math.PI / 12); // 15度
transformMatrix = rotateMatrix.multiply(transformMatrix);
draw();
});
document.getElementById('scaleBtn').addEventListener('click', () => {
// 创建缩放变换矩阵并与当前矩阵相乘
const scaleMatrix = new Matrix3().setScaling(1.2, 1.2);
transformMatrix = scaleMatrix.multiply(transformMatrix);
draw();
});
document.getElementById('resetBtn').addEventListener('click', () => {
// 重置变换矩阵
transformMatrix = new Matrix3();
draw();
});
</script>
</body>
</html>
八、总结
齐次坐标是计算机图形学中一个非常重要的概念,它通过增加一个维度,统一了点和向量的表示,并且能够用矩阵乘法简洁地表示各种变换。掌握齐次坐标和变换矩阵是理解和实现更复杂图形算法的基础。
通过本文的介绍和示例代码,你应该对齐次坐标有了基本的理解,并且能够实现简单的图形变换。在实际应用中,齐次坐标还广泛应用于 3D 图形、计算机视觉和机器人学等领域。