CSS Perspective 与 3D Transform 的空间矩阵原理
大家好!今天我们来深入探讨 CSS perspective
和 3D transform
的底层原理,特别是它们如何通过空间矩阵来实现 3D 变换。理解这些概念对于构建复杂的 3D CSS 动画和场景至关重要。
1. 3D 变换的基础:齐次坐标系
在进入矩阵之前,我们先要理解齐次坐标系。在传统的笛卡尔坐标系中,一个 3D 点由 (x, y, z) 表示。但在齐次坐标系中,我们使用 (x, y, z, w) 来表示,其中 w 是一个比例因子。
-
为什么要使用齐次坐标?
- 平移操作的矩阵表示: 齐次坐标允许我们用矩阵乘法同时表示旋转、缩放和平移等变换。 如果没有齐次坐标,平移操作将需要矩阵加法,导致操作不统一。
- 透视投影: 齐次坐标是实现透视投影的关键。 通过在变换过程中改变
w
值,并在投影到 2D 屏幕时进行归一化,我们可以模拟物体远小近大的透视效果。 - 仿射变换的统一表示: 齐次坐标可以统一表示仿射变换,包括线性变换(旋转、缩放、剪切)和平移。
-
齐次坐标与笛卡尔坐标的转换:
-
从齐次坐标 (x, y, z, w) 转换为笛卡尔坐标 (x’, y’, z’),需要进行归一化:
x' = x / w y' = y / w z' = z / w
-
从笛卡尔坐标 (x, y, z) 转换为齐次坐标 (x, y, z, 1)。
w
通常设为 1。
-
2. 变换矩阵:核心概念
变换矩阵是一个 4×4 的矩阵,它可以用来表示旋转、缩放、平移和透视投影等 3D 变换。 我们将齐次坐标表示的点乘以变换矩阵,得到变换后的齐次坐标。
[x', y', z', w'] = [x, y, z, 1] * M
其中 M
是 4×4 变换矩阵。
下面我们来分解几种常见的变换矩阵:
2.1 平移矩阵 (Translation Matrix)
平移矩阵用于将物体沿 x, y, z 轴移动。
[ 1 0 0 0 ]
[ 0 1 0 0 ]
[ 0 0 1 0 ]
[ Tx Ty Tz 1 ]
其中 Tx, Ty, Tz 分别是沿 x, y, z 轴的平移量。
示例代码:
function createTranslationMatrix(tx, ty, tz) {
return [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
tx, ty, tz, 1
];
}
let point = [1, 2, 3, 1]; // 齐次坐标
let translationMatrix = createTranslationMatrix(5, 10, 15);
let transformedPoint = multiplyMatrixAndPoint(point, translationMatrix);
console.log(transformedPoint); // 输出:[6, 12, 18, 1]
2.2 缩放矩阵 (Scale Matrix)
缩放矩阵用于沿 x, y, z 轴缩放物体。
[ Sx 0 0 0 ]
[ 0 Sy 0 0 ]
[ 0 0 Sz 0 ]
[ 0 0 0 1 ]
其中 Sx, Sy, Sz 分别是沿 x, y, z 轴的缩放比例。
示例代码:
function createScaleMatrix(sx, sy, sz) {
return [
sx, 0, 0, 0,
0, sy, 0, 0,
0, 0, sz, 0,
0, 0, 0, 1
];
}
let point = [1, 2, 3, 1];
let scaleMatrix = createScaleMatrix(2, 3, 4);
let transformedPoint = multiplyMatrixAndPoint(point, scaleMatrix);
console.log(transformedPoint); // 输出:[2, 6, 12, 1]
2.3 旋转矩阵 (Rotation Matrix)
旋转矩阵用于绕 x, y, z 轴旋转物体。 旋转矩阵相对复杂,我们分别给出绕三个轴旋转的矩阵:
-
绕 X 轴旋转:
[ 1 0 0 0 ] [ 0 cosθ -sinθ 0 ] [ 0 sinθ cosθ 0 ] [ 0 0 0 1 ]
-
绕 Y 轴旋转:
[ cosθ 0 sinθ 0 ] [ 0 1 0 0 ] [ -sinθ 0 cosθ 0 ] [ 0 0 0 1 ]
-
绕 Z 轴旋转:
[ cosθ -sinθ 0 0 ] [ sinθ cosθ 0 0 ] [ 0 0 1 0 ] [ 0 0 0 1 ]
其中 θ 是旋转角度 (弧度制)。
示例代码 (绕 Z 轴旋转):
function createRotationZMatrix(angle) {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
return [
cos, -sin, 0, 0,
sin, cos, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
];
}
let point = [1, 0, 0, 1];
let rotationMatrix = createRotationZMatrix(Math.PI / 2); // 旋转 90 度
let transformedPoint = multiplyMatrixAndPoint(point, rotationMatrix);
console.log(transformedPoint); // 输出:接近 [0, 1, 0, 1] (由于浮点数精度问题)
2.4 透视投影矩阵 (Perspective Projection Matrix)
透视投影矩阵用于模拟透视效果,使远处的物体看起来更小。 这是 perspective
属性的核心。
[ 1 0 0 0 ]
[ 0 1 0 0 ]
[ 0 0 1 -1/d ]
[ 0 0 0 1 ]
其中 d
是观察者到投影平面的距离,也就是 perspective
属性的值。 这个值越大,透视效果越弱;值越小,透视效果越强。
透视投影的原理:
透视投影矩阵的关键在于第三行第三列的 -1/d
。 它会影响齐次坐标的 w
值。 变换后的点的齐次坐标为 (x’, y’, z’, w’),其中 w’ = 1 – z/d。
然后,我们将齐次坐标转换为笛卡尔坐标:
x'' = x' / w' = x' / (1 - z/d)
y'' = y' / w' = y' / (1 - z/d)
z'' = z' / w' = z' / (1 - z/d)
可以看到,x''
和 y''
都除以了 (1 - z/d)
。 当 z
增加时,(1 - z/d)
减小,导致 x''
和 y''
增大。 反之,当 z
减小时,(1 - z/d)
增大,导致 x''
和 y''
减小。 这就是透视效果的来源:越远的点,其 x 和 y 坐标的绝对值越小,看起来也就越小。
示例代码:
function createPerspectiveMatrix(d) {
return [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, -1/d,
0, 0, 0, 1
];
}
let point = [1, 2, 10, 1]; // z = 10
let perspectiveMatrix = createPerspectiveMatrix(500); // d = 500
let transformedPoint = multiplyMatrixAndPoint(point, perspectiveMatrix);
console.log(transformedPoint); // 输出:[1, 2, 10, 0.98] (w 值发生变化)
let normalizedPoint = [
transformedPoint[0] / transformedPoint[3],
transformedPoint[1] / transformedPoint[3],
transformedPoint[2] / transformedPoint[3]
];
console.log(normalizedPoint); // 输出:[1.0204081632653061, 2.0408163265306123, 10.204081632653061] (x, y, z 坐标被缩小)
point = [1, 2, 100, 1]; // z = 100
transformedPoint = multiplyMatrixAndPoint(point, perspectiveMatrix);
normalizedPoint = [
transformedPoint[0] / transformedPoint[3],
transformedPoint[1] / transformedPoint[3],
transformedPoint[2] / transformedPoint[3]
];
console.log(normalizedPoint); // 输出:[1.0416666666666667, 2.0833333333333335, 104.16666666666667] (x, y, z 坐标被更大程度的缩小)
可以看出,z
值越大,归一化后的 x 和 y 坐标越小,透视效果越明显。
2.5 矩阵的组合:复合变换
我们可以将多个变换矩阵相乘,得到一个复合变换矩阵。 例如,要先旋转再平移,可以先计算旋转矩阵 R
,再计算平移矩阵 T
,然后将它们相乘: M = R * T
。 注意矩阵相乘的顺序非常重要,不同的顺序会得到不同的结果。 通常,我们按照缩放 -> 旋转 -> 平移的顺序进行变换。
示例代码:
function multiplyMatrices(a, b) {
let result = [];
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
let sum = 0;
for (let k = 0; k < 4; k++) {
sum += a[i * 4 + k] * b[k * 4 + j];
}
result[i * 4 + j] = sum;
}
}
return result;
}
let rotationMatrix = createRotationZMatrix(Math.PI / 4); // 旋转 45 度
let translationMatrix = createTranslationMatrix(5, 0, 0); // 平移 5 个单位
let combinedMatrix = multiplyMatrices(rotationMatrix, translationMatrix);
let point = [1, 1, 0, 1];
let transformedPoint = multiplyMatrixAndPoint(point, combinedMatrix);
console.log(transformedPoint);
3. CSS 中的 Perspective 属性
CSS perspective
属性设置了观察者与 z=0 平面的距离。它影响了 3D 变换的透视效果。 perspective
属性只能应用于元素的父元素。
.container {
perspective: 500px; /* 观察者距离 z=0 平面 500px */
}
.element {
transform: rotateX(45deg) translateZ(100px);
}
在这个例子中,.container
设置了 perspective
,.element
进行了 3D 变换。 translateZ(100px)
将元素沿 z 轴移动了 100px。 由于 perspective
的存在,这个移动会影响元素的透视效果,使其看起来更小或更大。
perspective-origin
属性:
perspective-origin
属性设置透视观察点的位置。 默认值是 center center
,表示观察点位于元素的中心。 可以通过 perspective-origin: x y;
来改变观察点的位置,其中 x
和 y
可以是百分比、像素值或关键字 (left, right, top, bottom, center)。
4. CSS 中的 Transform 属性
CSS transform
属性允许我们对元素进行 2D 或 3D 变换。 我们可以使用 transform
属性来应用旋转、缩放、平移和倾斜等变换。
3D Transform 函数:
translateX(tx)
: 沿 x 轴平移。translateY(ty)
: 沿 y 轴平移。translateZ(tz)
: 沿 z 轴平移。translate3d(tx, ty, tz)
: 沿 x, y, z 轴平移。scaleX(sx)
: 沿 x 轴缩放。scaleY(sy)
: 沿 y 轴缩放。scaleZ(sz)
: 沿 z 轴缩放。scale3d(sx, sy, sz)
: 沿 x, y, z 轴缩放。rotateX(angle)
: 绕 x 轴旋转。rotateY(angle)
: 绕 y 轴旋转。rotateZ(angle)
: 绕 z 轴旋转。rotate3d(x, y, z, angle)
: 绕任意向量 (x, y, z) 旋转。perspective(d)
: 设置透视距离 (不推荐直接在 transform 中使用,应该在父元素中使用 perspective 属性)。matrix3d(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p)
: 直接指定 4×4 变换矩阵的值。
transform-origin
属性:
transform-origin
属性设置变换的中心点。 默认值是 center center
。 可以通过 transform-origin: x y z;
来改变变换中心点的位置,其中 x
和 y
可以是百分比、像素值或关键字,z
是一个可选的 z 轴偏移量。
5. matrix3d()
函数:直接控制变换矩阵
matrix3d()
函数允许我们直接指定 4×4 变换矩阵的值。 这为我们提供了最大的灵活性,可以实现任何复杂的 3D 变换。
.element {
transform: matrix3d(
a, b, c, d,
e, f, g, h,
i, j, k, l,
m, n, o, p
);
}
其中 a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p 对应于 4×4 矩阵的 16 个元素:
[ a b c d ]
[ e f g h ]
[ i j k l ]
[ m n o p ]
例如,以下代码使用 matrix3d()
实现了一个简单的平移:
.element {
transform: matrix3d(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
50, 100, 0, 1 /* Tx = 50, Ty = 100 */
);
}
这等价于 transform: translateX(50px) translateY(100px);
。
6. 实践案例:构建一个 3D 立方体
现在,我们来使用 CSS 3D transform 和 perspective
属性构建一个简单的 3D 立方体。
HTML:
<div class="container">
<div class="cube">
<div class="face front">Front</div>
<div class="face back">Back</div>
<div class="face right">Right</div>
<div class="face left">Left</div>
<div class="face top">Top</div>
<div class="face bottom">Bottom</div>
</div>
</div>
CSS:
.container {
width: 200px;
height: 200px;
perspective: 800px; /* 设置透视距离 */
position: relative;
}
.cube {
width: 200px;
height: 200px;
position: absolute;
transform-style: preserve-3d; /* 关键:保持 3D 变换 */
transition: transform 1s; /* 添加过渡效果 */
transform: rotateX(45deg) rotateY(45deg); /* 初始旋转 */
}
.cube:hover {
transform: rotateX(60deg) rotateY(60deg); /* 鼠标悬停时旋转 */
}
.face {
width: 200px;
height: 200px;
position: absolute;
background-color: rgba(255, 0, 0, 0.5); /* 半透明红色 */
color: white;
font-size: 24px;
text-align: center;
line-height: 200px;
border: 1px solid black;
}
.front {
transform: translateZ(100px);
}
.back {
transform: translateZ(-100px) rotateY(180deg);
}
.right {
transform: translateX(100px) rotateY(90deg);
}
.left {
transform: translateX(-100px) rotateY(-90deg);
}
.top {
transform: translateY(-100px) rotateX(90deg);
}
.bottom {
transform: translateY(100px) rotateX(-90deg);
}
代码解释:
.container
设置了perspective
属性,创建了 3D 场景。.cube
设置了transform-style: preserve-3d;
。 这个属性非常重要,它告诉浏览器保持元素的 3D 变换,使其子元素也受到 3D 变换的影响。 如果没有这个属性,立方体的每个面都会被扁平化到 2D 平面上。.face
定义了立方体的每个面的样式。- 每个面的
transform
属性将其移动到立方体的正确位置和方向。 例如,.front
面沿 z 轴移动了 100px,.back
面沿 z 轴移动了 -100px 并绕 y 轴旋转了 180 度。
这个例子展示了如何使用 CSS perspective
和 3D transform
创建一个简单的 3D 物体。 通过改变 perspective
值、旋转角度和平移量,可以创建各种各样的 3D 效果。
7. 常见问题与注意事项
transform-style: preserve-3d
的重要性: 如果没有这个属性,3D 变换将不会正确应用到子元素上。- 矩阵乘法的顺序: 矩阵乘法的顺序非常重要,不同的顺序会得到不同的结果。 通常,按照缩放 -> 旋转 -> 平移的顺序进行变换。
perspective
属性的应用对象:perspective
属性只能应用于元素的父元素。- 性能问题: 复杂的 3D 变换可能会影响性能。 尽量减少不必要的变换,并使用硬件加速。
- Z-fighting: 当两个面非常接近时,可能会出现 Z-fighting 现象,导致渲染错误。 可以尝试调整元素的 z 轴位置来避免这个问题。
- 理解坐标系: 掌握3D坐标系对于进行3D变换至关重要。
8. 辅助函数
在前面的示例中,我们用到了multiplyMatrixAndPoint
和multiplyMatrices
两个函数,这里给出完整实现:
function multiplyMatrixAndPoint(point, matrix) {
let x = point[0] * matrix[0] + point[1] * matrix[4] + point[2] * matrix[8] + point[3] * matrix[12];
let y = point[0] * matrix[1] + point[1] * matrix[5] + point[2] * matrix[9] + point[3] * matrix[13];
let z = point[0] * matrix[2] + point[1] * matrix[6] + point[2] * matrix[10] + point[3] * matrix[14];
let w = point[0] * matrix[3] + point[1] * matrix[7] + point[2] * matrix[11] + point[3] * matrix[15];
return [x, y, z, w];
}
function multiplyMatrices(a, b) {
let result = [];
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
let sum = 0;
for (let k = 0; k < 4; k++) {
sum += a[i * 4 + k] * b[k * 4 + j];
}
result[i * 4 + j] = sum;
}
}
return result;
}
深入理解背后的数学原理
理解 CSS perspective
和 3D transform
背后的空间矩阵原理,能够帮助我们更好地控制 3D 场景,创建更复杂的 3D 动画和效果。 掌握齐次坐标系、变换矩阵和复合变换的概念,能够让我们更有效地利用 CSS 3D transform 的强大功能。