CSS 3D 引擎:仅用 `transform` 构建可交互的 3D 场景与第一人称视角

好的,我们开始。

引言:CSS 3D 的潜力与局限

CSS 3D 并非专为构建复杂的 3D 游戏引擎而设计。它更适合于创建简单的、视觉效果增强的 UI 元素和交互式演示。然而,通过巧妙地利用 CSS transform 属性,我们可以实现一个基本的可交互的 3D 场景,并模拟第一人称视角。关键在于理解 3D 变换的数学原理,以及如何使用 CSS 来表达这些变换。

3D 坐标系与变换矩阵

在 3D 空间中,我们使用右手坐标系。x 轴向右,y 轴向上,z 轴指向屏幕外。变换矩阵是一个 4×4 的矩阵,它可以描述平移、旋转、缩放和透视等变换。

| 变换类型 | 矩阵表示

说明: transforms
在CSS 中,例如translate3drotateXscale3, rotateYrotateZ等,实际上底层都使用了矩阵运算。浏览器会自动将这些更易读的写法转换为对应的变换矩阵。

平移矩阵:

[ 1  0  0  tx ]
[ 0  1  0  ty ]
[ 0  0  1  tz ]
[ 0  0  1  1 ]

其中,txtytz分别是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 ]

缩放矩阵:

[ sx  0   0  0 ]
[ 0   sy  0  0 ]
[ 0   0   sz  0 ]
[ 0   0   0  1 ]

sxsysz分别是x、y、z轴的缩放比例。

透视投影矩阵

虽然CSS transform 本身不直接提供透视投影矩阵的控制,但 perspective 属性会影响 3D 变换的效果,它本质上是在变换链的某个位置应用了一个透视投影变换。

矩阵的组合与顺序

多个变换矩阵可以相乘得到一个新的变换矩阵,代表组合变换。 变换顺序非常重要,旋转、缩放、平移的顺序不同,结果也不同。 通常建议的顺序是:缩放 -> 旋转 -> 平移。 CSS中的 transform 属性,变换顺序是从右向左的。 例如transform: translate3d(tx, ty, tz) rotateX(rx) scale3d(sx, sy, sz); 实际的变换顺序是 scale3d -> rotateX -> translate3d。

构建基本 3D 场景:立方体

让我们从一个简单的立方体开始。我们需要六个 div 元素,每个元素代表立方体的一个面。

<div class="container">
  <div class="cube">
    <div class="face front"></div>
    <div class="face back"></div>
    <div class="face right"></div>
    <div class="face left"></div>
    <div class="face top"></div>
    <div class="face bottom"></div>
  </div>
</div>
.container {
  width: 200px;
  height: 200px;
  position: relative;
  perspective: 800px; /* 添加透视效果 */
}

.cube {
  width: 200px;
  height: 200px;
  position: absolute;
  transform-style: preserve-3d; /* 关键:保留 3D 变换 */
}

.face {
  width: 200px;
  height: 200px;
  position: absolute;
  background-color: rgba(0, 0, 255, 0.5); /* 半透明蓝色 */
  border: 1px solid black;
  box-sizing: border-box;
}

.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); }

/* 可选:让立方体旋转 */
.cube {
  animation: rotateCube 10s linear infinite;
}

@keyframes rotateCube {
  from { transform: rotateX(0deg) rotateY(0deg); }
  to { transform: rotateX(360deg) rotateY(360deg); }
}

代码解释:

  • perspective: 在 .container 上设置透视效果。数值越小,透视效果越强。
  • transform-style: preserve-3d: 在 .cube 上设置,使得子元素(各个面)保持 3D 变换的上下文。如果没有这个属性,子元素将扁平化到 2D 平面。
  • .front, .back, .right, .left, .top, .bottom: 分别定位立方体的六个面。 使用 translateZ 将面移动到立方体的前方和后方,使用 translateXtranslateY 将面移动到立方体的侧面和顶部/底部,并使用 rotateYrotateX 旋转面,使它们正确朝向。
  • animation: 可选的,让立方体自动旋转。

实现第一人称视角

要实现第一人称视角,我们需要一个“相机”,它代表玩家的视角。我们可以通过改变相机的位置和旋转来模拟玩家的移动和视角转动。 在CSS中,我们实际上是通过反向变换整个场景来实现相机移动的效果。 例如,如果相机向 x 轴正方向移动 100px,我们实际上是将整个场景向 x 轴负方向移动 100px。

<div class="container">
  <div class="scene">
    <div class="cube">
      </div>
  </div>
</div>
.container {
  width: 500px;
  height: 500px;
  position: relative;
  overflow: hidden; /* 隐藏超出容器的内容 */
}

.scene {
  width: 1000px; /* 场景大于容器,允许移动 */
  height: 1000px;
  position: absolute;
  transform-style: preserve-3d;
  perspective: 800px;
  /* 初始相机位置 */
  transform: translateZ(-500px) rotateY(0deg) rotateX(0deg); /* 相机初始位置和朝向 */
}

.cube {
  width: 200px;
  height: 200px;
  position: absolute;
  transform-style: preserve-3d;
  transform: translate3d(0px, 0px, 0px); /* 立方体初始位置 */
}

.face { /* 同上 */ }
.front { /* 同上 */ }
.back { /* 同上 */ }
.right { /* 同上 */ }
.left { /* 同上 */ }
.top { /* 同上 */ }
.bottom { /* 同上 */ }
const scene = document.querySelector('.scene');
let cameraX = 0;
let cameraY = 0;
let cameraZ = -500;  // 相机初始位置
let rotationX = 0; // X轴旋转角度
let rotationY = 0; // Y轴旋转角度
const moveSpeed = 10; // 移动速度
const rotateSpeed = 2; // 旋转速度

function updateCamera() {
  scene.style.transform = `translate3d(${cameraX}px, ${cameraY}px, ${cameraZ}px) rotateX(${rotationX}deg) rotateY(${rotationY}deg)`;
}

document.addEventListener('keydown', (event) => {
  switch (event.key) {
    case 'w': // 前进
      cameraZ += moveSpeed * Math.cos(rotationY * Math.PI / 180);
      cameraX += moveSpeed * Math.sin(rotationY * Math.PI / 180);
      break;
    case 's': // 后退
      cameraZ -= moveSpeed * Math.cos(rotationY * Math.PI / 180);
      cameraX -= moveSpeed * Math.sin(rotationY * Math.PI / 180);
      break;
    case 'a': // 左移
      cameraX -= moveSpeed * Math.cos(rotationY * Math.PI / 180);
      cameraZ += moveSpeed * Math.sin(rotationY * Math.PI / 180);
      break;
    case 'd': // 右移
      cameraX += moveSpeed * Math.cos(rotationY * Math.PI / 180);
      cameraZ -= moveSpeed * Math.sin(rotationY * Math.PI / 180);
      break;
    case 'ArrowLeft': // 左转
      rotationY -= rotateSpeed;
      break;
    case 'ArrowRight': // 右转
      rotationY += rotateSpeed;
      break;
    case 'ArrowUp': // 上转
        rotationX -= rotateSpeed;
        break;
    case 'ArrowDown': // 下转
        rotationX += rotateSpeed;
        break;
  }
  updateCamera();
});

代码解释:

  • cameraX, cameraY, cameraZ: 相机在世界坐标系中的位置。
  • rotationX, rotationY: 相机的旋转角度。
  • updateCamera(): 更新 .scenetransform 属性,模拟相机移动。 注意,这里我们使用反向变换。
  • keydown 事件监听器: 监听键盘事件,根据按键改变相机的位置和旋转。 我们使用 Math.cosMath.sin 来计算相机在 xz 轴上的移动量,以实现平滑的移动。

碰撞检测

虽然 CSS 3D 本身不提供碰撞检测,但我们可以使用 JavaScript 来实现简单的碰撞检测。 以下是一个简单的例子,检测相机是否与立方体碰撞。

function checkCollision() {
  const cubeX = 0; // 立方体中心 X 坐标
  const cubeY = 0; // 立方体中心 Y 坐标
  const cubeZ = 0; // 立方体中心 Z 坐标
  const cubeSize = 200; // 立方体大小

  // 计算相机到立方体中心的距离
  const distanceX = cameraX - cubeX;
  const distanceY = cameraY - cubeY;
  const distanceZ = cameraZ - cubeZ;

  // 如果相机在立方体内部,则发生碰撞
  if (
    distanceX > -cubeSize / 2 &&
    distanceX < cubeSize / 2 &&
    distanceY > -cubeSize / 2 &&
    distanceY < cubeSize / 2 &&
    distanceZ > -cubeSize / 2 &&
    distanceZ < cubeSize / 2
  ) {
    alert('碰撞!');
    // 重置相机位置
    cameraX = 0;
    cameraY = 0;
    cameraZ = -500;
    updateCamera();
  }
}

document.addEventListener('keydown', (event) => {
    // ... (之前的键盘事件处理代码)

    updateCamera();
    checkCollision(); // 检查碰撞
});

代码解释:

  • checkCollision(): 计算相机到立方体中心的距离。 如果相机在立方体内部,则认为发生了碰撞。
  • keydown 事件监听器: 在每次相机移动后,调用 checkCollision() 检查碰撞。

优化与局限性

CSS 3D 引擎的性能瓶颈在于浏览器的渲染能力。 复杂的场景和大量的元素会显著降低性能。 以下是一些优化建议:

  • 减少元素数量: 尽量减少场景中的元素数量。 使用 CSS clip-path 可以创建复杂的形状,而无需使用大量的 div 元素。
  • 简化几何体: 简化 3D 模型的几何体。 例如,可以用简单的立方体代替复杂的模型。
  • 使用纹理: 使用纹理可以增加场景的细节,而无需增加元素数量。 可以使用 CSS background-image 属性来设置纹理。
  • 避免过度动画: 避免过度使用动画。 动画会消耗大量的 CPU 和 GPU 资源。
  • 使用 will-change will-change 属性可以提示浏览器哪些元素将会被修改,从而优化渲染性能。 例如,可以设置 will-change: transform

局限性:

  • 性能: CSS 3D 的性能不如 WebGL。 不适合构建复杂的 3D 游戏。
  • 功能: CSS 3D 的功能有限。 不支持高级特性,例如光照、阴影和纹理映射。
  • 碰撞检测: CSS 3D 本身不提供碰撞检测。 需要使用 JavaScript 来实现。
  • 复杂性: 构建复杂的 3D 场景需要大量的 CSS 和 JavaScript 代码。

扩展场景:添加更多物体

为了丰富场景,我们可以添加更多的立方体或其他形状。 关键在于控制它们在 scene 中的位置,并且确保它们与相机的交互符合预期。

<div class="container">
    <div class="scene">
        <div class="cube" id="cube1"></div>
        <div class="cube" id="cube2"></div>
        <!-- More cubes... -->
    </div>
</div>

#cube1 {
    transform: translate3d(200px, 0px, 200px); /* Cube 1 position */
}

#cube2 {
    transform: translate3d(-200px, 0px, -200px); /* Cube 2 position */
}
/* ... more cube styles with positions ... */

在这个扩展的例子中,每个立方体都有一个唯一的 ID,并且使用 translate3d 放置在场景的不同位置。 注意,碰撞检测函数需要相应地更新,以检测与所有立方体的碰撞。

使用 JavaScript 控制动画

除了 CSS 动画,我们还可以使用 JavaScript 来控制动画,提供更灵活的控制。 例如,我们可以根据玩家的输入来改变立方体的旋转。

function animateCubes() {
  const cube1 = document.getElementById('cube1');
  const cube2 = document.getElementById('cube2');

  cube1.style.transform = `translate3d(200px, 0px, 200px) rotateY(${Date.now() / 10}deg)`;
  cube2.style.transform = `translate3d(-200px, 0px, -200px) rotateX(${Date.now() / 15}deg)`;

  requestAnimationFrame(animateCubes); // Request the next frame
}

animateCubes(); // Start the animation loop

这段代码使用 requestAnimationFrame 创建一个动画循环,每帧更新立方体的 transform 属性,使它们旋转。

更好的视角控制:鼠标锁定和鼠标移动

为了更自然地控制视角,我们可以使用鼠标锁定 API 和鼠标移动事件。

document.addEventListener('mousedown', function() {
  container.requestPointerLock(); // 请求鼠标锁定
});

document.addEventListener('mousemove', function(e) {
  if (document.pointerLockElement === container) {
    rotationY += e.movementX * 0.1;
    rotationX -= e.movementY * 0.1;
    rotationX = Math.max(-90, Math.min(90, rotationX)); // 限制垂直视角
    updateCamera();
  }
});

document.addEventListener('pointerlockchange', function() {
  if (document.pointerLockElement !== container) {
    // 鼠标解锁时的处理
  }
});

代码解释:

  • requestPointerLock(): 请求浏览器锁定鼠标指针到 container 元素。
  • mousemove 事件监听器: 当鼠标被锁定时,监听鼠标移动事件,根据鼠标移动的距离更新 rotationXrotationY
  • pointerlockchange 事件监听器: 监听鼠标锁定状态的改变。

总结:用CSS构建3D世界

通过巧妙地运用CSS的transform属性,我们能够构建出简单的可交互3D场景,并模拟第一人称视角。虽然这种方法存在性能限制,但它在创建轻量级3D效果和增强UI交互方面具有潜力。深入理解3D变换的数学原理是关键,这将帮助我们更好地利用CSS的强大功能。

更多IT精英技术系列讲座,到智猿学院

发表回复

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