在计算机图形学的广阔领域中,如何让屏幕上的像素动起来、变大小或者旋转起来,是最基础也是最迷人的话题之一。今天,我们将一起深入探索 2D 变换 的世界。我们将以专业的视角,详细剖析其中的核心概念——缩放,并顺带探讨与之紧密相关的旋转和平移变换。我们将从数学原理出发,一步步推导到 C 语言代码实现,确保你不仅“知其然”,更能“知其所以然”。
探索线性变换的数学基础
在开始动手写代码之前,我们需要先建立数学思维。我们可以使用一个 $2 \times 2$ 的矩阵来改变或变换一个 2D 向量。这种操作接收一个 2 维向量,通过矩阵乘法生成一个新的 2 维向量,这就是我们常说的线性变换。
通过这个简单的公式,根据我们在矩阵中填入的数值,我们可以实现各种有用的变换。为了便于理解,我们可以把沿 x 轴的移动视为水平移动,而沿 y 轴的移动视为垂直移动。这种数学模型是所有现代图形引擎(如 OpenGL, DirectX)的基石。
核心概念:缩放变换
缩放变换是我们改变对象大小的手段。在这个过程中,我们要么压缩对象的尺寸,要么对其进行扩展。缩放操作可以通过将多边形的每个顶点坐标 $(x, y)$ 与缩放因子 $Sx$ 和 $Sy$ 相乘来实现,从而生成变换后的坐标 $(x‘, y‘)$。
公式如下:
$$ x‘ = x \cdot S_x $$
$$ y‘ = y \cdot S_y $$
这里,缩放因子 $Sx$ 和 $Sy$ 分别沿 X 和 Y 方向对对象进行缩放。因此,上述方程可以用矩阵形式优雅地表示为:
$$ \begin{bmatrix} X‘ \\ Y‘ \end{bmatrix}=\begin{bmatrix} Sx & 0 \\ 0 & Sy \end{bmatrix}\begin{bmatrix} X \\ Y \end{bmatrix} $$
或者简记为向量形式:$P‘ = S \cdot P$。
#### 缩放过程的可视化
想象一下,你有一个正方形。如果你将 $Sx$ 和 $Sy$ 都设为 2,正方形的面积会变为原来的 4 倍,同时位置也会远离原点(如果原点不在中心)。这就引出了一个重要的实际概念:固定点缩放。
> 注意:
> 1. 如果缩放因子 $S$ 小于 1,我们会缩小对象的尺寸。
> 2. 如果缩放因子 $S$ 大于 1,我们会增大对象的尺寸。
> 3. 关键点: 如果 $Sx$ 或 $Sy$ 为负数,我们还可以实现对象的镜像翻转。
#### 算法逻辑
为了在代码中实现缩放,我们通常遵循以下步骤:
- 构建矩阵:创建一个 2×2 的缩放矩阵 $S$,其中对角线元素分别为 $Sx$ 和 $Sy$。
- 遍历顶点:对于多边形的每一个点 $(x, y)$:
(i) 创建一个坐标矩阵 $P$。
(ii) 执行矩阵乘法 $S \times P$ 得到新坐标 $(x‘, y‘)$。
- 重绘对象:使用新坐标绘制多边形。
#### 实战代码:C 语言实现缩放
下面是一段经典的 C 语言实现,使用了 graphics.h 库。我们将展示如何让用户输入多边形顶点,并执行缩放操作。
#include
#include
#include
#include
#include
int gd = DETECT, gm;
int n, x[100], y[100], i;
float sfx, sfy;
// 函数声明
void draw();
void scale();
using namespace std;
int main() {
// 1. 获取多边形信息
cout <> n;
cout << "请依次输入每个顶点的坐标 ";
for (i = 0; i > x[i] >> y[i];
}
// 2. 获取缩放因子
cout <> sfx >> sfy;
// 3. 初始化图形模式
initgraph(&gd, &gm, (char*)"");
cleardevice();
// 4. 绘制原始图形(红色)
setcolor(RED);
draw();
// 5. 执行缩放计算
scale();
// 6. 绘制缩放后图形(黄色)
setcolor(YELLOW);
draw();
getch();
closegraph();
return 0;
}
// 绘制多边形的函数
void draw() {
for (i = 0; i < n; i++) {
// 使用取模运算连接最后一个点和第一个点
line(x[i], y[i], x[(i + 1) % n], y[(i + 1) % n]);
}
}
// 执行缩放的函数
// 注意:这里演示的是相对于第一个顶点(x[0], y[0])的缩放
void scale() {
for (i = 0; i < n; i++) {
// 先将点移动到以(x[0], y[0])为原点的坐标系中
// 乘以缩放因子
// 再移回原来的坐标系位置
x[i] = x[0] + (int)((float)(x[i] - x[0]) * sfx);
y[i] = y[0] + (int)((float)(y[i] - y[0]) * sfx); // 注意:原代码此处可能有意使用sfx统一缩放,若需Y轴独立缩放请改为sfy
}
}
#### 代码深度解析与优化
在上述代码中,你可能注意到了 INLINECODEaa322f95 函数并不是简单地将 x 和 y 乘以因子。为什么要写成 INLINECODEa40ca5b0 这样复杂的形式?
这就是关于固定点的缩放。如果我们直接计算 INLINECODE89b9c29e,对象会相对于屏幕原点 $(0,0)$ 进行缩放。这意味着对象不仅会变大,还会向右上方“飘走”。在实际应用中,我们通常希望对象相对于其自身中心或者某个特定顶点进行缩放。上述代码通过减去 INLINECODE9ad27c12(将原点临时移到该顶点),缩放后再加回去,实现了相对于第一个顶点的缩放。
进阶:旋转变换
除了缩放,旋转是另一个核心变换。旋转变换涉及到三角函数的应用。假设我们要将点 $(x, y)$ 绕原点逆时针旋转 $ heta$ 角度,新的坐标 $(x‘, y‘)$ 计算公式如下:
$$ x‘ = x \cos \theta – y \sin \theta $$
$$ y‘ = x \sin \theta + y \cos \theta $$
写成矩阵形式则是:
$$ \begin{bmatrix} X‘ \\ Y‘ \end{bmatrix}=\begin{bmatrix} \cos \theta & -\sin \theta \\ \sin \theta & \cos \theta \end{bmatrix}\begin{bmatrix} X \\ Y \end{bmatrix} $$
让我们看看如何在 C 语言中实现这一过程。
#include
#include
#include
#include
using namespace std;
int main() {
int gd = 0, gm; // 图形驱动和模式
int x1, y1, x2, y2, x3, y3; // 三角形的三个顶点
double s, c, angle; // 正弦值, 余弦值, 角度
initgraph(&gd, &gm, (char*)"");
cout <> x1 >> y1 >> x2 >> y2 >> x3 >> y3;
setbkcolor(WHITE);
cleardevice();
// 绘制原始三角形(红色)
setcolor(RED);
line(x1, y1, x2, y2);
line(x2, y2, x3, y3);
line(x3, y3, x1, y1);
cout <> angle;
// 将角度转换为弧度,并计算三角函数值
// 注意:C语言中的sin/cos函数使用弧度制
double rad = angle * M_PI / 180.0;
c = cos(rad);
s = sin(rad);
// 应用旋转公式
// 注意:这里直接取整可能会导致精度损失,但在像素级显示中通常可以接受
int newx1 = floor(x1 * c - y1 * s);
int newy1 = floor(x1 * s + y1 * c);
int newx2 = floor(x2 * c - y2 * s);
int newy2 = floor(x2 * s + y2 * c);
int newx3 = floor(x3 * c - y3 * s);
int newy3 = floor(x3 * s + y3 * c);
// 绘制旋转后的三角形(绿色)
setcolor(GREEN);
line(newx1, newy1, newx2, newy2);
line(newx2, newy2, newx3, newy3);
line(newx3, newy3, newx1, newy1);
getch();
closegraph();
return 0;
}
基础:平移变换
平移是最简单的变换,它只是在对象的每个坐标上加上一个偏移量 $(Tx, Ty)$。
$$ x‘ = x + T_x $$
$$ y‘ = y + T_y $$
这个操作无法通过 $2 \times 2$ 矩阵乘法直接实现(因为加法无法表示为乘法),这也是为什么在高级计算机图形学中我们会引入 齐次坐标 和 $3 \times 3$ 矩阵的原因(为了统一平移、旋转和缩放)。
以下是平移的实现代码,展示了如何让对象在屏幕上移动。
#include
#include
#include
#include
int gd = DETECT, gm;
int n, xs[100], ys[100], i, ty, tx;
void draw();
void translate();
using namespace std;
int main() {
cout <> n;
cout << "请依次输入每个顶点的坐标 :";
for (i = 0; i > xs[i] >> ys[i];
}
cout <> tx >> ty;
initgraph(&gd, &gm, (char*)"");
cleardevice();
// 绘制原始位置(红色)
setcolor(RED);
draw();
// 执行平移计算
translate();
// 绘制平移后位置(黄色)
setcolor(YELLOW);
draw();
getch();
closegraph();
return 0;
}
void draw() {
for (i = 0; i < n; i++) {
line(xs[i], ys[i], xs[(i + 1) % n], ys[(i + 1) % n]);
}
}
void translate() {
for (i = 0; i < n; i++) {
xs[i] = xs[i] + tx;
ys[i] = ys[i] + ty;
}
}
总结与最佳实践
在这篇文章中,我们深入探讨了计算机图形学中最基本的三大变换:缩放、旋转和平移。
- 数据结构的选择:虽然我们使用了简单的数组来存储坐标,但在实际的大型图形引擎中,我们会使用结构体
struct Point { float x, y; }或者专门的向量类来管理数据,这样代码会更清晰,且易于扩展到 3D 空间。 - 浮点数精度:在图形计算中,尽量在中间步骤保持 INLINECODEe9a3f6b0 或 INLINECODEfaf4832e 类型,只在最后绘制时转换为
int。过早的取整会导致严重的精度丢失,特别是在连续变换时。 - 坐标系中心:我们在缩放代码中看到了相对于固定点缩放的重要性。这是初学者最容易犯错的地方——忘记将对象移回原位,导致对象“飞”出屏幕。
- 变换的组合:在实际应用中,我们很少只做一种变换。比如,游戏中的角色可能是先缩放(变大),然后旋转(转身),最后平移(移动)。这就涉及到了复合变换矩阵的计算,这也是从这些基础代码迈向现代图形 API 的关键一步。
希望这篇详细的解析和代码示例能帮助你建立起对 2D 变换的直观理解。不妨试着修改上面的代码,实现一个同时绕自身中心旋转并缩放的动画吧!