9-3 二维坐标系转换

二维坐标系转换(2D Changing Coordinate Systems)

在前面的章节中,我们使用矩阵来变换——对同一个坐标系中的点施加旋转、平移和缩放。但在实际应用中,常常需要处理多个坐标系之间的关系:例如游戏中的"世界坐标"与"角色本地坐标",或者机器人学中的"基座标系"与"关节坐标系"。

坐标系转换(Changing Coordinate Systems)的核心思想是:同一个点在不同坐标系中有不同的坐标表示,而矩阵可以描述这些表示之间的转换关系。这不是"移动点",而是"换一种视角来看同一个点"。

这一概念是场景图(Scene Graph)、骨骼动画(Skeletal Animation)和机器人运动学(Robot Kinematics)的基础。


坐标系(Coordinate Frame)

一个二维坐标系(Coordinate Frame)由两个要素定义:

  1. 原点(Origin):坐标系的原点在世界坐标中的位置 (ox, oy)
  2. 基向量(Basis Vectors):两个方向向量,分别表示该坐标系的 x 轴方向和 y 轴方向
世界坐标系 (World Frame):
  - 原点在 (0, 0)
  - x 轴方向 (1, 0),y 轴方向 (0, 1)

本地坐标系 (Local Frame):
  - 原点在世界坐标 (ox, oy)
  - x 轴方向 (îx, îy),y 轴方向 (ĵx, ĵy)

用矩阵来表示一个坐标系:

| îx  ĵx  ox |
| îy  ĵy  oy |
|  0    0   1 |
  • 前两列 [îx, îy][ĵx, ĵy] 是基向量(basis vectors),描述坐标轴的方向和缩放
  • 第三列 [ox, oy] 是原点在世界坐标中的位置
  • 第三行 [0, 0, 1] 是齐次坐标的固定行

关键理解:这个矩阵同时也是"从本地坐标转换到世界坐标"的变换矩阵。


本地坐标到世界坐标(Local → World)

假设有一个本地坐标系,其原点在世界坐标 (3, 2),x 轴方向为 (1, 0)、y 轴方向为 (0, 1)(即未旋转,仅平移)。一个本地坐标为 (1, 1) 的点,在世界坐标中的位置为:

Frame Matrix:         Local point:    World point:
| 1  0  3 |          | 1 |          | 1*1 + 0*1 + 3 |   | 4 |
| 0  1  2 |    ×     | 1 |    =     | 0*1 + 1*1 + 2 | = | 3 |
| 0  0  1 |          | 1 |          |       1       |   | 1 |

这就是"本地坐标 → 世界坐标"的转换:将本地坐标点 (1, 1) 转换为世界坐标 (4, 3)。

更一般的例子:一个旋转过的本地坐标系。原点在世界 (2, 1),坐标轴旋转了 90 度(逆时针)。此时:

  • 旋转 90 度后,x 轴方向变为 (0, 1),y 轴方向变为 (-1, 0)
  • 坐标系矩阵为:
|  0  -1  2 |
|  1   0  1 |
|  0   0  1 |

本地坐标 (1, 0) 的点,在世界坐标中:

|  0  -1  2 |   | 1 |   | 0*1 + (-1)*0 + 2 |   | 2 |
|  1   0  1 | × | 0 | = | 1*1 +  0*0  + 1  | = | 2 |
|  0   0  1 |   | 1 |   |        1         |   | 1 |

结果:本地 (1, 0) 对应世界 (2, 2)。直观理解:本地 x 轴方向是世界的"上"方向,所以沿本地 x 轴走 1 单位,在世界中是 y 方向 +1。


世界坐标到本地坐标(World → Local)

已知世界坐标,要求本地坐标,需要使用坐标系矩阵的逆矩阵(Inverse Matrix):

P_local = Frame^(-1) × P_world

对于上面的旋转 90 度的坐标系(原点 (2, 1)),求世界坐标 (2, 2) 对应的本地坐标:

Frame = |  0  -1  2 |
        |  1   0  1 |
        |  0   0  1 |

Frame^(-1) = |  0   1  -1 |     (旋转逆 = 旋转 -90 度)
             | -1   0   2 |     (平移逆 = 反向平移)
             |  0   0   1 |

Frame^(-1) × (2, 2, 1):
|  0   1  -1 |   | 2 |   | 0*2 + 1*2 + (-1) |   | 1 |
| -1   0   2 | × | 2 | = | (-1)*2 + 0*2 + 2 | = | 0 |
|  0   0   1 |   | 1 |   |        1         |   | 1 |

结果为 (1, 0),与之前的计算完全吻合。

对于仿射变换矩阵(最后一行为 [0, 0, 1]),逆矩阵的计算公式为:

| a  b  tx |^(-1)      1      |  d  -b  (b*ty - d*tx) |
| c  d  ty |       = ------- × | -c   a  (c*tx - a*ty) |
| 0  0   1 |         det      |  0   0      det        |

其中 det = a*d - b*c(2x2 子矩阵的行列式)

C++ 实现

#include <iostream>
#include <cmath>
#include <iomanip>
using namespace std;

// 3x3 matrix for coordinate frame
struct Frame2D {
    double m[3][3];

    // Create frame from origin and angle (radians)
    static Frame2D fromOriginAngle(double ox, double oy, double angle) {
        Frame2D f;
        double c = cos(angle), s = sin(angle);
        // Column 1: x-axis direction (rotated)
        f.m[0][0] = c;   f.m[1][0] = s;
        // Column 2: y-axis direction (rotated)
        f.m[0][1] = -s;  f.m[1][1] = c;
        // Column 3: origin position
        f.m[0][2] = ox;  f.m[1][2] = oy;
        // Row 3: homogeneous row
        f.m[2][0] = 0;   f.m[2][1] = 0;  f.m[2][2] = 1;
        return f;
    }

    // Local to world: P_world = Frame * P_local
    void localToWorld(double lx, double ly, double& wx, double& wy) const {
        wx = m[0][0] * lx + m[0][1] * ly + m[0][2];
        wy = m[1][0] * lx + m[1][1] * ly + m[1][2];
    }

    // World to local: P_local = Frame^(-1) * P_world
    void worldToLocal(double wx, double wy, double& lx, double& ly) const {
        double det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
        double dx = wx - m[0][2];
        double dy = wy - m[1][2];
        lx = ( m[1][1] * dx - m[0][1] * dy) / det;
        ly = (-m[1][0] * dx + m[0][0] * dy) / det;
    }
};

int main() {
    double ox = 2, oy = 1;
    double angle = M_PI / 2;  // 90 degrees

    Frame2D frame = Frame2D::fromOriginAngle(ox, oy, angle);

    cout << fixed << setprecision(4);
    cout << "Frame origin: (" << ox << ", " << oy << ")" << endl;
    cout << "Frame angle: 90 degrees" << endl;
    cout << "Frame matrix:" << endl;
    for (int i = 0; i < 3; i++) {
        cout << "  | ";
        for (int j = 0; j < 3; j++)
            cout << setw(6) << frame.m[i][j] << " ";
        cout << "|" << endl;
    }

    // Local to world
    double lx = 1, ly = 0;
    double wx, wy;
    frame.localToWorld(lx, ly, wx, wy);
    cout << "\nLocal (" << lx << ", " << ly << ") -> World ("
         << wx << ", " << wy << ")" << endl;

    // World to local (inverse)
    double lx2, ly2;
    frame.worldToLocal(wx, wy, lx2, ly2);
    cout << "World (" << wx << ", " << wy << ") -> Local ("
         << lx2 << ", " << ly2 << ")" << endl;

    return 0;
}

C 实现

#include <stdio.h>
#include <math.h>

typedef double Frame2D[3][3];

// Create frame from origin and angle (radians)
void frame2d_from_origin_angle(double ox, double oy, double angle, Frame2D f) {
    double c = cos(angle), s = sin(angle);
    f[0][0] = c;   f[1][0] = s;  f[2][0] = 0;
    f[0][1] = -s;  f[1][1] = c;  f[2][1] = 0;
    f[0][2] = ox;  f[1][2] = oy; f[2][2] = 1;
}

// Local to world: P_world = Frame * P_local
void frame2d_local_to_world(Frame2D f, double lx, double ly,
                            double* wx, double* wy) {
    *wx = f[0][0] * lx + f[0][1] * ly + f[0][2];
    *wy = f[1][0] * lx + f[1][1] * ly + f[1][2];
}

// World to local: P_local = Frame^(-1) * P_world
void frame2d_world_to_local(Frame2D f, double wx, double wy,
                            double* lx, double* ly) {
    double det = f[0][0] * f[1][1] - f[0][1] * f[1][0];
    double dx = wx - f[0][2];
    double dy = wy - f[1][2];
    *lx = ( f[1][1] * dx - f[0][1] * dy) / det;
    *ly = (-f[1][0] * dx + f[0][0] * dy) / det;
}

int main() {
    double ox = 2, oy = 1;
    double angle = M_PI / 2;

    Frame2D frame;
    frame2d_from_origin_angle(ox, oy, angle, frame);

    printf("Frame origin: (%g, %g)\n", ox, oy);
    printf("Frame angle: 90 degrees\n");

    double lx = 1, ly = 0;
    double wx, wy;
    frame2d_local_to_world(frame, lx, ly, &wx, &wy);
    printf("\nLocal (%g, %g) -> World (%g, %g)\n", lx, ly, wx, wy);

    double lx2, ly2;
    frame2d_world_to_local(frame, wx, wy, &lx2, &ly2);
    printf("World (%g, %g) -> Local (%g, %g)\n", wx, wy, lx2, ly2);

    return 0;
}

Python 实现

import math

class Frame2D:
    """2D coordinate frame defined by origin and rotation angle."""

    def __init__(self, ox, oy, angle):
        """Create frame from origin (ox, oy) and rotation angle (radians)."""
        c, s = math.cos(angle), math.sin(angle)
        # Column 1: x-axis, Column 2: y-axis, Column 3: origin
        self.m = [
            [c, -s, ox],
            [s,  c, oy],
            [0,  0,  1]
        ]

    def local_to_world(self, lx, ly):
        """Convert local coordinates to world coordinates."""
        wx = self.m[0][0] * lx + self.m[0][1] * ly + self.m[0][2]
        wy = self.m[1][0] * lx + self.m[1][1] * ly + self.m[1][2]
        return wx, wy

    def world_to_local(self, wx, wy):
        """Convert world coordinates to local coordinates."""
        det = self.m[0][0] * self.m[1][1] - self.m[0][1] * self.m[1][0]
        dx = wx - self.m[0][2]
        dy = wy - self.m[1][2]
        lx = ( self.m[1][1] * dx - self.m[0][1] * dy) / det
        ly = (-self.m[1][0] * dx + self.m[0][0] * dy) / det
        return lx, ly


ox, oy = 2, 1
angle = math.pi / 2

frame = Frame2D(ox, oy, angle)

print(f"Frame origin: ({ox}, {oy})")
print("Frame angle: 90 degrees")

lx, ly = 1, 0
wx, wy = frame.local_to_world(lx, ly)
print(f"\nLocal ({lx}, {ly}) -> World ({wx}, {wy})")

lx2, ly2 = frame.world_to_local(wx, wy)
print(f"World ({wx}, {wy}) -> Local ({lx2}, {ly2})")

Go 实现

package main

import (
	"fmt"
	"math"
)

// Frame2D represents a 2D coordinate frame as a 3x3 matrix
type Frame2D [3][3]float64

// newFrame2D creates a frame from origin (ox, oy) and rotation angle (radians)
func newFrame2D(ox, oy, angle float64) Frame2D {
	c, s := math.Cos(angle), math.Sin(angle)
	return Frame2D{
		{c, -s, ox},
		{s, c, oy},
		{0, 0, 1},
	}
}

// localToWorld converts local coordinates to world coordinates
func (f Frame2D) localToWorld(lx, ly float64) (float64, float64) {
	wx := f[0][0]*lx + f[0][1]*ly + f[0][2]
	wy := f[1][0]*lx + f[1][1]*ly + f[1][2]
	return wx, wy
}

// worldToLocal converts world coordinates to local coordinates
func (f Frame2D) worldToLocal(wx, wy float64) (float64, float64) {
	det := f[0][0]*f[1][1] - f[0][1]*f[1][0]
	dx := wx - f[0][2]
	dy := wy - f[1][2]
	lx := (f[1][1]*dx - f[0][1]*dy) / det
	ly := (-f[1][0]*dx + f[0][0]*dy) / det
	return lx, ly
}

func main() {
	ox, oy := 2.0, 1.0
	angle := math.Pi / 2

	frame := newFrame2D(ox, oy, angle)

	fmt.Printf("Frame origin: (%g, %g)\n", ox, oy)
	fmt.Println("Frame angle: 90 degrees")

	lx, ly := 1.0, 0.0
	wx, wy := frame.localToWorld(lx, ly)
	fmt.Printf("\nLocal (%g, %g) -> World (%g, %g)\n", lx, ly, wx, wy)

	lx2, ly2 := frame.worldToLocal(wx, wy)
	fmt.Printf("World (%g, %g) -> Local (%g, %g)\n", wx, wy, lx2, ly2)
}

运行该程序将输出:

Frame origin: (2, 1)
Frame angle: 90 degrees

Local (1, 0) -> World (2, 2)
World (2, 2) -> Local (1, 0)

嵌套坐标系(Nested Coordinate Frames)

坐标系转换最强大的地方在于处理嵌套的层级坐标系。例如:

世界坐标系 (World)
  └── 房间坐标系 (Room)
        └── 桌子坐标系 (Table)
              └── 杯子坐标系 (Cup)

杯子在桌子上的本地坐标,需要经过多次转换才能得到世界坐标:

P_world = M_room × M_table × M_cup × P_cup_local

每一层只关心自己相对于父级坐标系的关系,这种分层设计极大地简化了复杂场景的管理。

以一个具体例子说明:

  • 房间原点在世界 (10, 5),未旋转
  • 桌子原点在房间 (3, 2),未旋转
  • 杯子原点在桌子 (1, 1),旋转 90 度
  • 杯子中有一个点,本地坐标 (0.5, 0)
Step 1: 杯子→桌子
  M_cup = | 0  -1  1 |   本地点 (0.5, 0)
          | 1   0  1 |
          | 0   0  1 |
  M_cup × (0.5, 0, 1) = (0*0.5 + (-1)*0 + 1, 1*0.5 + 0*0 + 1, 1)
                       = (1, 1.5, 1)  ← 桌子坐标

Step 2: 桌子→房间
  M_table = | 1  0  3 |   桌子点 (1, 1.5)
            | 0  1  2 |
            | 0  0  1 |
  M_table × (1, 1.5, 1) = (1+3, 1.5+2, 1)
                         = (4, 3.5, 1)  ← 房间坐标

Step 3: 房间→世界
  M_room = | 1  0  10 |   房间点 (4, 3.5)
           | 0  1   5 |
           | 0  0   1 |
  M_room × (4, 3.5, 1) = (4+10, 3.5+5, 1)
                        = (14, 8.5, 1)  ← 世界坐标

结果:杯中的本地点 (0.5, 0) 在世界坐标中为 (14, 8.5)。

C++ 实现

#include <iostream>
#include <cmath>
#include <iomanip>
using namespace std;

// 3x3 matrix for coordinate frames
struct Frame2D {
    double m[3][3];

    static Frame2D fromOriginAngle(double ox, double oy, double angle) {
        Frame2D f;
        double c = cos(angle), s = sin(angle);
        f.m[0][0] = c;   f.m[0][1] = -s; f.m[0][2] = ox;
        f.m[1][0] = s;   f.m[1][1] = c;  f.m[1][2] = oy;
        f.m[2][0] = 0;   f.m[2][1] = 0;  f.m[2][2] = 1;
        return f;
    }

    // Matrix multiplication: this * other
    Frame2D multiply(const Frame2D& other) const {
        Frame2D result;
        for (int i = 0; i < 3; i++)
            for (int j = 0; j < 3; j++) {
                result.m[i][j] = 0;
                for (int k = 0; k < 3; k++)
                    result.m[i][j] += m[i][k] * other.m[k][j];
            }
        return result;
    }

    // Transform point
    void transform(double x, double y, double& ox, double& oy) const {
        ox = m[0][0] * x + m[0][1] * y + m[0][2];
        oy = m[1][0] * x + m[1][1] * y + m[1][2];
    }
};

int main() {
    // Build the frame hierarchy
    Frame2D room  = Frame2D::fromOriginAngle(10, 5, 0);       // World offset
    Frame2D table = Frame2D::fromOriginAngle(3, 2, 0);        // Room offset
    Frame2D cup   = Frame2D::fromOriginAngle(1, 1, M_PI / 2); // Table offset, rotated

    // Composite: M_room * M_table * M_cup
    Frame2D tableInWorld = room.multiply(table);
    Frame2D cupInWorld   = tableInWorld.multiply(cup);

    double lx = 0.5, ly = 0;
    double wx, wy;
    cupInWorld.transform(lx, ly, wx, wy);

    cout << fixed << setprecision(1);
    cout << "Cup local point: (" << lx << ", " << ly << ")" << endl;
    cout << "World point:     (" << wx << ", " << wy << ")" << endl;

    return 0;
}

C 实现

#include <stdio.h>
#include <math.h>

typedef double Frame2D[3][3];

void frame2d_from_origin_angle(double ox, double oy, double angle, Frame2D f) {
    double c = cos(angle), s = sin(angle);
    f[0][0] = c;   f[0][1] = -s; f[0][2] = ox;
    f[1][0] = s;   f[1][1] = c;  f[1][2] = oy;
    f[2][0] = 0;   f[2][1] = 0;  f[2][2] = 1;
}

void frame2d_multiply(Frame2D A, Frame2D B, Frame2D result) {
    Frame2D temp = {{0}};
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            for (int k = 0; k < 3; k++)
                temp[i][j] += A[i][k] * B[k][j];
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            result[i][j] = temp[i][j];
}

void frame2d_transform(Frame2D f, double x, double y, double* ox, double* oy) {
    *ox = f[0][0] * x + f[0][1] * y + f[0][2];
    *oy = f[1][0] * x + f[1][1] * y + f[1][2];
}

int main() {
    Frame2D room, table, cup;
    frame2d_from_origin_angle(10, 5, 0, room);
    frame2d_from_origin_angle(3, 2, 0, table);
    frame2d_from_origin_angle(1, 1, M_PI / 2, cup);

    Frame2D tableInWorld, cupInWorld;
    frame2d_multiply(room, table, tableInWorld);
    frame2d_multiply(tableInWorld, cup, cupInWorld);

    double lx = 0.5, ly = 0;
    double wx, wy;
    frame2d_transform(cupInWorld, lx, ly, &wx, &wy);

    printf("Cup local point: (%g, %g)\n", lx, ly);
    printf("World point:     (%g, %g)\n", wx, wy);

    return 0;
}

Python 实现

import math

class Frame2D:
    """2D coordinate frame."""

    def __init__(self, ox, oy, angle):
        c, s = math.cos(angle), math.sin(angle)
        self.m = [[c, -s, ox], [s, c, oy], [0, 0, 1]]

    def multiply(self, other):
        result = Frame2D(0, 0, 0)
        for i in range(3):
            for j in range(3):
                result.m[i][j] = sum(
                    self.m[i][k] * other.m[k][j] for k in range(3)
                )
        return result

    def transform(self, x, y):
        ox = self.m[0][0]*x + self.m[0][1]*y + self.m[0][2]
        oy = self.m[1][0]*x + self.m[1][1]*y + self.m[1][2]
        return ox, oy


# Build frame hierarchy
room  = Frame2D(10, 5, 0)
table = Frame2D(3, 2, 0)
cup   = Frame2D(1, 1, math.pi / 2)

# Composite: Room * Table * Cup
cup_in_world = room.multiply(table).multiply(cup)

lx, ly = 0.5, 0
wx, wy = cup_in_world.transform(lx, ly)

print(f"Cup local point: ({lx}, {ly})")
print(f"World point:     ({wx}, {wy})")

Go 实现

package main

import (
	"fmt"
	"math"
)

type Frame2D [3][3]float64

func newFrame2D(ox, oy, angle float64) Frame2D {
	c, s := math.Cos(angle), math.Sin(angle)
	return Frame2D{
		{c, -s, ox},
		{s, c, oy},
		{0, 0, 1},
	}
}

func (a Frame2D) multiply(b Frame2D) Frame2D {
	var result Frame2D
	for i := 0; i < 3; i++ {
		for j := 0; j < 3; j++ {
			for k := 0; k < 3; k++ {
				result[i][j] += a[i][k] * b[k][j]
			}
		}
	}
	return result
}

func (f Frame2D) transform(x, y float64) (float64, float64) {
	ox := f[0][0]*x + f[0][1]*y + f[0][2]
	oy := f[1][0]*x + f[1][1]*y + f[1][2]
	return ox, oy
}

func main() {
	room := newFrame2D(10, 5, 0)
	table := newFrame2D(3, 2, 0)
	cup := newFrame2D(1, 1, math.Pi/2)

	cupInWorld := room.multiply(table).multiply(cup)

	lx, ly := 0.5, 0.0
	wx, wy := cupInWorld.transform(lx, ly)

	fmt.Printf("Cup local point: (%g, %g)\n", lx, ly)
	fmt.Printf("World point:     (%g, %g)\n", wx, wy)
}

运行该程序将输出:

Cup local point: (0.5, 0)
World point:     (14, 8.5)

坐标系转换 vs 点变换

坐标系转换和点变换使用的是同一个矩阵,但解释方式不同:

概念 操作 含义
点变换 P' = M × P 将点 P 移动到新位置 P'(同一个坐标系)
坐标系转换 P_global = M × P_local 同一个点在两个坐标系中的不同表示

这两者的数学运算完全相同——都是 3x3 矩阵乘以齐次坐标向量。区别在于我们如何理解结果:

  • 点变换视角:世界坐标系是固定的,点在"移动"
  • 坐标系转换视角:点是固定的,我们换了一个"角度"来描述它

这种对偶性(Duality)在实际应用中非常有用。例如,将一个物体从 A 处移到 B 处(点变换),等价于将坐标系从 B 处切换到 A 处(坐标系转换)。矩阵相同,只是解读不同。


沿坐标轴方向的移动

在本地坐标系中,"前进 1 单位"意味着沿着本地 x 轴方向移动。这在游戏开发中极为常见——角色"向前走"就是沿本地 x 轴移动,但需要知道这对应世界坐标的哪个方向。

// 角色面向 θ 方向,前进 d 单位
world_dx = d * cos(θ)
world_dy = d * sin(θ)
new_x = old_x + world_dx
new_y = old_y + world_dy

这实际上就是坐标系矩阵的第一列(本地 x 轴在世界中的方向)乘以移动距离 d,再加上当前位置。

C++ 实现

#include <iostream>
#include <cmath>
using namespace std;

int main() {
    double x = 3, y = 2;         // Current position
    double heading = M_PI / 4;   // 45 degrees (northeast)
    double distance = 2;         // Move forward 2 units

    // Move along local x-axis (heading direction)
    double dx = distance * cos(heading);
    double dy = distance * sin(heading);
    double nx = x + dx;
    double ny = y + dy;

    cout << "Start:     (" << x << ", " << y << ")" << endl;
    cout << "Heading:   45 degrees" << endl;
    cout << "Distance:  " << distance << endl;
    cout << "New pos:   (" << nx << ", " << ny << ")" << endl;

    return 0;
}

C 实现

#include <stdio.h>
#include <math.h>

int main() {
    double x = 3, y = 2;
    double heading = M_PI / 4;
    double distance = 2;

    double dx = distance * cos(heading);
    double dy = distance * sin(heading);
    double nx = x + dx;
    double ny = y + dy;

    printf("Start:     (%g, %g)\n", x, y);
    printf("Heading:   45 degrees\n");
    printf("Distance:  %g\n", distance);
    printf("New pos:   (%g, %g)\n", nx, ny);

    return 0;
}

Python 实现

import math

x, y = 3, 2
heading = math.pi / 4  # 45 degrees
distance = 2

dx = distance * math.cos(heading)
dy = distance * math.sin(heading)
nx = x + dx
ny = y + dy

print(f"Start:     ({x}, {y})")
print("Heading:   45 degrees")
print(f"Distance:  {distance}")
print(f"New pos:   ({nx}, {ny})")

Go 实现

package main

import (
	"fmt"
	"math"
)

func main() {
	x, y := 3.0, 2.0
	heading := math.Pi / 4
	distance := 2.0

	dx := distance * math.Cos(heading)
	dy := distance * math.Sin(heading)
	nx := x + dx
	ny := y + dy

	fmt.Printf("Start:     (%g, %g)\n", x, y)
	fmt.Println("Heading:   45 degrees")
	fmt.Printf("Distance:  %g\n", distance)
	fmt.Printf("New pos:   (%g, %g)\n", nx, ny)
}

运行该程序将输出:

Start:     (3, 2)
Heading:   45 degrees
Distance:  2
New pos:   (4.41421, 3.41421)

二维坐标系转换的性质

矩阵即坐标系

一个 3x3 仿射变换矩阵可以同时理解为两种含义:

  • 变换矩阵:描述如何变换点
  • 坐标系描述:描述一个本地坐标系的朝向和位置
| îx  ĵx  ox |     =     | x轴方向  y轴方向  原点位置 |
| îy  ĵy  oy |
|  0    0   1 |

链式法则

嵌套坐标系的转换遵循链式法则:

P_world = M1 × M2 × M3 × ... × Mn × P_local

矩阵从右到左依次作用于点。可以将部分矩阵预先相乘(pre-multiply),减少运行时的计算量:

M_total = M1 × M2 × M3
P_world = M_total × P_local  // 只需一次矩阵乘法

逆变换

从世界坐标回到本地坐标需要逆矩阵:

P_local = M^(-1) × P_world

对于只包含旋转和平移的坐标系(刚体变换),逆矩阵的计算非常高效:

| R  t |^(-1)   | R^T  -R^T × t |
| 0  1 |      = |  0       1     |

其中 R^T 是旋转矩阵的转置(也是其逆矩阵),不需要真正的矩阵求逆。

应用场景

应用 说明
游戏开发 世界坐标 ↔ 角色/物体本地坐标
机器人学 基座标系 ↔ 各关节坐标系
场景图 父节点 ↔ 子节点坐标系
GUI 布局 窗口坐标 ↔ 控件本地坐标
SVG/CSS 用户空间 ↔ 设备空间
posted @ 2026-04-17 23:42  游翔  阅读(11)  评论(0)    收藏  举报