3D图形学中的平面方程:从线性代数到Vector4f表示

在3D图形学编程中,平面的数学表示是一个基础且重要的概念。无论是进行碰撞检测、视锥体裁剪,还是实现复杂的渲染算法,理解平面方程的数学原理都至关重要。本文将深入探讨3D空间中平面的各种表示方法,特别是点法式方程和Vector4f表示法,以及法向量朝向的重要性。

线性代数基础:什么是平面

平面的几何定义

在三维空间中,平面是一个二维的几何对象,具有以下特性:

  • 无限延伸的平坦表面
  • 任意两点之间的直线完全位于平面内
  • 由一个点和一个法向量唯一确定

平面的数学本质

从线性代数的角度看,三维空间中的平面实际上是一个线性方程的解集

1
ax + by + cz + d = 0

其中 (x, y, z) 是平面上任意一点的坐标,(a, b, c) 构成平面的法向量,d 是常数项。

点法式方程:平面的经典表示

什么是点法式方程

点法式方程是描述平面最直观的方法之一。如果我们知道:

  • 平面上的一个已知点 P0(x0, y0, z0)
  • 平面的法向量 n = (a, b, c)

那么平面上任意一点 P(x, y, z) 都满足:

1
n · (P - P0) = 0

数学推导过程

让我们详细推导这个方程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 设平面上一个已知点为 P0(x0, y0, z0)
// 平面法向量为 n = (a, b, c)
// 平面上任意一点为 P(x, y, z)

// 向量 P0P = P - P0 = (x-x0, y-y0, z-z0)
// 由于 P0P 在平面内,所以它与法向量垂直
// 因此:n · P0P = 0

// 展开点积:
a(x - x0) + b(y - y0) + c(z - z0) = 0

// 整理得到:
ax + by + cz - (ax0 + by0 + cz0) = 0

// 令 d = -(ax0 + by0 + cz0),得到一般式:
ax + by + cz + d = 0

实际代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>
#include <vector>

struct Point3D {
float x, y, z;
Point3D(float x, float y, float z) : x(x), y(y), z(z) {}
};

struct Vector3D {
float x, y, z;
Vector3D(float x, float y, float z) : x(x), y(y), z(z) {}

// 点积运算
float dot(const Vector3D& other) const {
return x * other.x + y * other.y + z * other.z;
}
};

class Plane {
private:
Vector3D normal; // 法向量 (a, b, c)
float d; // 常数项

public:
// 使用点法式构造平面
Plane(const Point3D& point, const Vector3D& normal)
: normal(normal) {
// 计算 d = -(ax0 + by0 + cz0)
d = -(normal.x * point.x + normal.y * point.y + normal.z * point.z);
}

// 检查点是否在平面上
bool isPointOnPlane(const Point3D& point, float epsilon = 1e-6f) const {
float result = normal.x * point.x + normal.y * point.y + normal.z * point.z + d;
return std::abs(result) < epsilon;
}

// 计算点到平面的距离
float distanceToPoint(const Point3D& point) const {
float numerator = std::abs(normal.x * point.x + normal.y * point.y + normal.z * point.z + d);
float denominator = std::sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z);
return numerator / denominator;
}

void printEquation() const {
std::cout << normal.x << "x + " << normal.y << "y + " << normal.z << "z + " << d << " = 0" << std::endl;
}
};

// 使用示例
int main() {
// 定义一个平面:通过点(1, 2, 3),法向量(1, 0, 0)
Point3D knownPoint(1, 2, 3);
Vector3D normalVector(1, 0, 0);

Plane plane(knownPoint, normalVector);
plane.printEquation(); // 输出:1x + 0y + 0z + -1 = 0

// 测试几个点
Point3D testPoint1(1, 5, 8); // 应该在平面上
Point3D testPoint2(2, 5, 8); // 不在平面上

std::cout << "点(1,5,8)在平面上: " << plane.isPointOnPlane(testPoint1) << std::endl;
std::cout << "点(2,5,8)到平面距离: " << plane.distanceToPoint(testPoint2) << std::endl;

return 0;
}

Vector4f表示法:ax + by + cz + d = 0

为什么使用Vector4f

在计算机图形学中,将平面方程 ax + by + cz + d = 0 的系数排列成一个四维向量 (a, b, c, d) 有诸多优势:

  1. 内存紧凑性:四个浮点数连续存储,便于GPU处理
  2. 计算效率:可以使用SIMD指令进行向量化计算
  3. 统一接口:与齐次坐标系统完美配合
  4. 矩阵运算:便于进行平面变换

Vector4f的存储格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <array>
#include <immintrin.h> // for SIMD

class Vector4f {
public:
union {
struct { float x, y, z, w; };
struct { float a, b, c, d; }; // 用于平面方程
std::array<float, 4> data;
__m128 simd; // SIMD寄存器
};

Vector4f(float a, float b, float c, float d) : a(a), b(b), c(c), d(d) {}

// 计算点到平面的符号距离
float signedDistanceToPoint(const Point3D& point) const {
return a * point.x + b * point.y + c * point.z + d;
}

// 归一化法向量(保持d不变)
Vector4f normalized() const {
float length = std::sqrt(a * a + b * b + c * c);
return Vector4f(a / length, b / length, c / length, d / length);
}
};

// 使用SIMD进行快速计算
class SIMDPlane {
private:
__m128 plane_eq; // 存储 (a, b, c, d)

public:
SIMDPlane(float a, float b, float c, float d) {
plane_eq = _mm_set_ps(d, c, b, a); // 注意参数顺序
}

// 同时计算多个点到平面的距离
void distanceToPoints(const std::vector<Point3D>& points, std::vector<float>& distances) {
distances.resize(points.size());

for (size_t i = 0; i < points.size(); i += 4) {
// 加载4个点的坐标
__m128 x = _mm_set_ps(
i + 3 < points.size() ? points[i + 3].x : 0,
i + 2 < points.size() ? points[i + 2].x : 0,
i + 1 < points.size() ? points[i + 1].x : 0,
points[i].x
);
// ... 类似地加载y, z坐标

// 执行向量化计算
__m128 result = _mm_mul_ps(plane_eq, x);
// ... 后续SIMD操作
}
}
};

signedDistanceToPoint函数详解

这个函数是3D图形学中的核心函数,让我详细解释其实现原理:

1. 数学原理

1
2
3
float signedDistanceToPoint(const Point3D& point) const {
return a * point.x + b * point.y + c * point.z + d;
}

这个函数直接实现了平面方程:

1
ax + by + cz + d = 0

当我们把点 P(x, y, z) 的坐标代入这个方程时:

1
f(P) = ax + by + cz + d

这个值 f(P) 就是我们要的符号距离

2. 为什么叫”符号距离”?

这个函数返回的值有特殊的几何意义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 假设平面方程为:2x + 3y - z + 5 = 0
// 法向量 n = (2, 3, -1),指向平面的一侧

Point3D point1(1, 1, 1);
float dist1 = plane.signedDistanceToPoint(point1);
// 结果:2*1 + 3*1 + (-1)*1 + 5 = 9 > 0
// 表示点在法向量指向的一侧

Point3D point2(-1, -1, -1);
float dist2 = plane.signedDistanceToPoint(point2);
// 结果:2*(-1) + 3*(-1) + (-1)*(-1) + 5 = -1 < 0
// 表示点在法向量指向的反侧

Point3D point3(0, 0, 5); // 这个点在平面上
float dist3 = plane.signedDistanceToPoint(point3);
// 结果:2*0 + 3*0 + (-1)*5 + 5 = 0
// 表示点在平面上

3. 符号的含义

1
2
3
4
5
// 对于平面方程 ax + by + cz + d = 0:
//
// 如果 f(P) > 0:点在法向量指向的一侧
// 如果 f(P) = 0:点在平面上
// 如果 f(P) < 0:点在法向量指向的反侧

4. 实际应用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Plane {
private:
Vector4f equation; // (a, b, c, d)

public:
float signedDistanceToPoint(const Point3D& point) const {
return equation.a * point.x + equation.b * point.y + equation.c * point.z + equation.d;
}

// 使用符号距离进行点分类
enum class PointLocation { Inside, OnPlane, Outside };

PointLocation classifyPoint(const Point3D& point, float epsilon = 1e-6f) const {
float distance = signedDistanceToPoint(point);

if (std::abs(distance) < epsilon) {
return PointLocation::OnPlane; // 点在平面上
} else if (distance < 0) {
return PointLocation::Inside; // 点在"内侧"
} else {
return PointLocation::Outside; // 点在"外侧"
}
}

// 计算点到平面的实际距离(绝对值)
float distanceToPoint(const Point3D& point) const {
float signedDist = signedDistanceToPoint(point);
float normalLength = std::sqrt(
equation.a * equation.a +
equation.b * equation.b +
equation.c * equation.c
);
return std::abs(signedDist) / normalLength;
}
};

5. 性能优势

这个函数非常高效,因为:

  1. 计算简单:只需要4次乘法和3次加法
  2. 内存友好:Vector4f的4个分量连续存储,缓存友好
  3. SIMD优化:可以轻松向量化处理多个点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SIMD优化版本
void testMultiplePoints(const Vector4f& plane,
const std::vector<Point3D>& points,
std::vector<float>& distances) {
__m128 plane_simd = _mm_load_ps(&plane.a);

for (size_t i = 0; i < points.size(); i += 4) {
// 加载4个点的坐标
__m128 x = _mm_set_ps(points[i+3].x, points[i+2].x, points[i+1].x, points[i].x);
__m128 y = _mm_set_ps(points[i+3].y, points[i+2].y, points[i+1].y, points[i].y);
__m128 z = _mm_set_ps(points[i+3].z, points[i+2].z, points[i+1].z, points[i].z);

// 计算 ax + by + cz + d
__m128 result = _mm_fmadd_ps(_mm_shuffle_ps(plane_simd, plane_simd, _MM_SHUFFLE(0,0,0,0)), x,
_mm_fmadd_ps(_mm_shuffle_ps(plane_simd, plane_simd, _MM_SHUFFLE(1,1,1,1)), y,
_mm_fmadd_ps(_mm_shuffle_ps(plane_simd, plane_simd, _MM_SHUFFLE(2,2,2,2)), z,
_mm_shuffle_ps(plane_simd, plane_simd, _MM_SHUFFLE(3,3,3,3)))));

_mm_store_ps(&distances[i], result);
}
}

6. 总结

这个 signedDistanceToPoint 函数是3D图形学中的核心函数,它:

  1. 直接实现平面方程ax + by + cz + d
  2. 提供符号信息:正负号表示点在平面的哪一侧
  3. 高效计算:只需要基本的算术运算
  4. 广泛应用:在碰撞检测、视锥体裁剪、CSG操作中都有重要应用

理解这个函数的原理对于掌握3D数学和图形编程至关重要。

齐次坐标中的应用

在齐次坐标系统中,平面的Vector4f表示尤其有用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 齐次坐标中的点 (x, y, z, w),其中 w = 1 表示位置
struct HomogeneousPoint {
float x, y, z, w;
HomogeneousPoint(float x, float y, float z) : x(x), y(y), z(z), w(1.0f) {}
};

// 平面方程在齐次坐标中的计算
float planePointTest(const Vector4f& plane, const HomogeneousPoint& point) {
// 这就是一个简单的4D点积!
return plane.a * point.x + plane.b * point.y + plane.c * point.z + plane.d * point.w;
}

// 矩阵变换平面
Vector4f transformPlane(const Vector4f& plane, const Matrix4x4& transform) {
// 平面变换需要使用变换矩阵的逆转置
Matrix4x4 invTranspose = transform.inverse().transpose();
return invTranspose * plane; // 矩阵乘向量
}

法向量朝向:Inward-Facing Normals详解

什么是Inward-Facing Normal

在用户提到的注释中:

1
/** Normals(a, b, c) of planes are all facing inward */

这里的”inward-facing”指的是法向量指向某个几何体或区域的内部。这在3D图形学中有重要的实际意义。

为什么需要规定法向量方向

1. 碰撞检测中的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class ConvexHull {
private:
std::vector<Vector4f> planes; // 所有平面的法向量都指向内部

public:
// 检查点是否在凸包内部
bool isPointInside(const Point3D& point) const {
for (const auto& plane : planes) {
float distance = plane.signedDistanceToPoint(point);
if (distance > 0) {
// 如果点在任何一个平面的"外侧",则不在凸包内
return false;
}
}
return true; // 点在所有平面的"内侧"
}

// 计算点到凸包边界的最近距离
float distanceToSurface(const Point3D& point) const {
float maxDistance = std::numeric_limits<float>::lowest();

for (const auto& plane : planes) {
float distance = plane.signedDistanceToPoint(point);
maxDistance = std::max(maxDistance, distance);
}

return maxDistance; // 正值表示在外部,负值表示在内部
}
};

2. 视锥体裁剪(Frustum Culling)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class ViewFrustum {
private:
// 6个平面:近、远、左、右、上、下,法向量都指向视锥体内部
std::array<Vector4f, 6> planes;

public:
void updateFromCamera(const Matrix4x4& viewProjectionMatrix) {
// 从视图投影矩阵提取裁剪平面
// 左平面: row4 + row1
planes[0] = Vector4f(
viewProjectionMatrix[3][0] + viewProjectionMatrix[0][0],
viewProjectionMatrix[3][1] + viewProjectionMatrix[0][1],
viewProjectionMatrix[3][2] + viewProjectionMatrix[0][2],
viewProjectionMatrix[3][3] + viewProjectionMatrix[0][3]
).normalized();

// 右平面: row4 - row1
planes[1] = Vector4f(
viewProjectionMatrix[3][0] - viewProjectionMatrix[0][0],
viewProjectionMatrix[3][1] - viewProjectionMatrix[0][1],
viewProjectionMatrix[3][2] - viewProjectionMatrix[0][2],
viewProjectionMatrix[3][3] - viewProjectionMatrix[0][3]
).normalized();

// ... 类似地计算其他平面
}

// 检查球体是否与视锥体相交
bool sphereIntersectsFrustum(const Point3D& center, float radius) const {
for (const auto& plane : planes) {
float distance = plane.signedDistanceToPoint(center);
if (distance > radius) {
// 球心到平面的距离大于半径,球体完全在视锥体外
return false;
}
}
return true;
}

// 检查AABB包围盒是否与视锥体相交
bool aabbIntersectsFrustum(const Point3D& min, const Point3D& max) const {
for (const auto& plane : planes) {
// 计算AABB在平面法向量方向上的最远点
Point3D farthestPoint(
plane.a > 0 ? max.x : min.x,
plane.b > 0 ? max.y : min.y,
plane.c > 0 ? max.z : min.z
);

if (plane.signedDistanceToPoint(farthestPoint) > 0) {
// 最远点都在平面外侧,整个AABB在视锥体外
return false;
}
}
return true;
}
};

Inward-Facing的数学含义

符号约定的重要性

当我们说法向量”facing inward”时,实际上是在建立一个符号约定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 对于inward-facing normal的平面方程 ax + by + cz + d = 0:
//
// 如果 ax + by + cz + d < 0:点在"内侧"(法向量指向的反方向)
// 如果 ax + by + cz + d = 0:点在平面上
// 如果 ax + by + cz + d > 0:点在"外侧"(法向量指向的方向)

class OrientedPlane {
private:
Vector4f equation; // (a, b, c, d)
bool isInwardFacing;

public:
OrientedPlane(const Vector4f& eq, bool inward = true)
: equation(eq), isInwardFacing(inward) {}

// 获取点相对于平面的位置
enum class PointLocation { Inside, OnPlane, Outside };

PointLocation classifyPoint(const Point3D& point, float epsilon = 1e-6f) const {
float distance = equation.signedDistanceToPoint(point);

if (std::abs(distance) < epsilon) {
return PointLocation::OnPlane;
}

// 根据法向量朝向调整判断逻辑
if (isInwardFacing) {
return (distance < 0) ? PointLocation::Inside : PointLocation::Outside;
} else {
return (distance > 0) ? PointLocation::Inside : PointLocation::Outside;
}
}

// 翻转法向量方向
OrientedPlane flip() const {
return OrientedPlane(Vector4f(-equation.a, -equation.b, -equation.c, -equation.d),
!isInwardFacing);
}
};

实际应用场景

1. 游戏引擎中的碰撞体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class CollisionMesh {
private:
std::vector<Vector4f> inwardFacingPlanes;

public:
// 从三角网格构建碰撞平面
void buildFromTriangles(const std::vector<Triangle>& triangles, const Point3D& interiorPoint) {
for (const auto& triangle : triangles) {
Vector3D normal = triangle.calculateNormal();
Point3D planePoint = triangle.vertices[0];

// 构造平面方程
float d = -(normal.x * planePoint.x + normal.y * planePoint.y + normal.z * planePoint.z);
Vector4f plane(normal.x, normal.y, normal.z, d);

// 检查法向量方向,确保指向内部
float testDistance = plane.signedDistanceToPoint(interiorPoint);
if (testDistance > 0) {
// 法向量指向外部,需要翻转
plane = Vector4f(-plane.a, -plane.b, -plane.c, -plane.d);
}

inwardFacingPlanes.push_back(plane);
}
}

bool containsPoint(const Point3D& point) const {
for (const auto& plane : inwardFacingPlanes) {
if (plane.signedDistanceToPoint(point) > 0) {
return false; // 点在某个平面的外侧
}
}
return true;
}
};

2. CSG(Constructive Solid Geometry)操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class CSGOperation {
public:
// 两个凸体的交集
static ConvexHull intersection(const ConvexHull& a, const ConvexHull& b) {
ConvexHull result;

// 交集的边界由两个凸体的所有inward-facing平面组成
for (const auto& plane : a.getPlanes()) {
result.addPlane(plane);
}
for (const auto& plane : b.getPlanes()) {
result.addPlane(plane);
}

return result;
}

// 从凸体A中减去凸体B
static ConvexHull difference(const ConvexHull& a, const ConvexHull& b) {
ConvexHull result;

// 保留A的所有inward-facing平面
for (const auto& plane : a.getPlanes()) {
result.addPlane(plane);
}

// 将B的平面翻转为outward-facing,然后添加
for (const auto& plane : b.getPlanes()) {
Vector4f flippedPlane(-plane.a, -plane.b, -plane.c, -plane.d);
result.addPlane(flippedPlane);
}

return result;
}
};

性能优化技巧

1. SIMD优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <immintrin.h>

class SIMDPlaneOperations {
public:
// 同时测试4个点与平面的关系
static void testFourPoints(const Vector4f& plane,
const Point3D points[4],
float results[4]) {
// 加载平面系数
__m128 plane_simd = _mm_load_ps(&plane.a);

// 加载4个点的x坐标
__m128 x_coords = _mm_set_ps(points[3].x, points[2].x, points[1].x, points[0].x);
__m128 y_coords = _mm_set_ps(points[3].y, points[2].y, points[1].y, points[0].y);
__m128 z_coords = _mm_set_ps(points[3].z, points[2].z, points[1].z, points[0].z);

// 计算 ax, by, cz
__m128 ax = _mm_mul_ps(_mm_shuffle_ps(plane_simd, plane_simd, _MM_SHUFFLE(0,0,0,0)), x_coords);
__m128 by = _mm_mul_ps(_mm_shuffle_ps(plane_simd, plane_simd, _MM_SHUFFLE(1,1,1,1)), y_coords);
__m128 cz = _mm_mul_ps(_mm_shuffle_ps(plane_simd, plane_simd, _MM_SHUFFLE(2,2,2,2)), z_coords);

// 加上常数项d
__m128 d_term = _mm_shuffle_ps(plane_simd, plane_simd, _MM_SHUFFLE(3,3,3,3));

// 计算最终结果:ax + by + cz + d
__m128 result = _mm_add_ps(_mm_add_ps(ax, by), _mm_add_ps(cz, d_term));

// 存储结果
_mm_store_ps(results, result);
}
};

2. 预计算优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class OptimizedFrustum {
private:
struct PrecomputedPlane {
Vector4f equation;
Vector3D normal;
float normalLength;
float invNormalLength;
};

std::array<PrecomputedPlane, 6> planes;

public:
void precomputePlaneData() {
for (auto& plane : planes) {
plane.normalLength = std::sqrt(
plane.equation.a * plane.equation.a +
plane.equation.b * plane.equation.b +
plane.equation.c * plane.equation.c
);
plane.invNormalLength = 1.0f / plane.normalLength;
plane.normal = Vector3D(
plane.equation.a * plane.invNormalLength,
plane.equation.b * plane.invNormalLength,
plane.equation.c * plane.invNormalLength
);
}
}

// 快速距离计算(避免重复的平方根运算)
float fastSignedDistance(int planeIndex, const Point3D& point) const {
const auto& plane = planes[planeIndex];
float unnormalizedDistance =
plane.equation.a * point.x +
plane.equation.b * point.y +
plane.equation.c * point.z +
plane.equation.d;
return unnormalizedDistance * plane.invNormalLength;
}
};

总结

本文深入探讨了3D空间中平面的数学表示和实际应用:

  1. 点法式方程提供了理解平面几何意义的直观方法
  2. Vector4f表示法为计算机图形学提供了高效的数据结构
  3. Inward-facing normals的约定在碰撞检测、视锥体裁剪等应用中至关重要
  4. 性能优化技术可以显著提升大规模几何计算的效率

理解这些概念不仅有助于掌握3D数学的基础,更能为实际的图形编程项目提供坚实的理论支撑。无论是开发游戏引擎、CAD软件还是科学可视化应用,平面方程都是不可或缺的数学工具。

相关参考

实践建议

  1. 动手实现:尝试自己实现一个简单的3D碰撞检测系统
  2. 可视化调试:使用图形调试工具来可视化平面和法向量
  3. 性能测试:比较不同实现方式的性能差异
  4. 数值稳定性:关注浮点数精度对几何计算的影响