研究 CSS perspective 与 3D transform 的空间矩阵原理

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; 来改变观察点的位置,其中 xy 可以是百分比、像素值或关键字 (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; 来改变变换中心点的位置,其中 xy 可以是百分比、像素值或关键字,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. 辅助函数

在前面的示例中,我们用到了multiplyMatrixAndPointmultiplyMatrices两个函数,这里给出完整实现:

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 的强大功能。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注