好的,我们开始。
引言:CSS 3D 的潜力与局限
CSS 3D 并非专为构建复杂的 3D 游戏引擎而设计。它更适合于创建简单的、视觉效果增强的 UI 元素和交互式演示。然而,通过巧妙地利用 CSS transform 属性,我们可以实现一个基本的可交互的 3D 场景,并模拟第一人称视角。关键在于理解 3D 变换的数学原理,以及如何使用 CSS 来表达这些变换。
3D 坐标系与变换矩阵
在 3D 空间中,我们使用右手坐标系。x 轴向右,y 轴向上,z 轴指向屏幕外。变换矩阵是一个 4×4 的矩阵,它可以描述平移、旋转、缩放和透视等变换。
| 变换类型 | 矩阵表示
说明: transforms
在CSS 中,例如translate3d、rotateX、scale3, rotateY、rotateZ等,实际上底层都使用了矩阵运算。浏览器会自动将这些更易读的写法转换为对应的变换矩阵。
平移矩阵:
[ 1 0 0 tx ]
[ 0 1 0 ty ]
[ 0 0 1 tz ]
[ 0 0 1 1 ]
其中,tx、ty、tz分别是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 ]
sx、sy、sz分别是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将面移动到立方体的前方和后方,使用translateX和translateY将面移动到立方体的侧面和顶部/底部,并使用rotateY和rotateX旋转面,使它们正确朝向。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(): 更新.scene的transform属性,模拟相机移动。 注意,这里我们使用反向变换。keydown事件监听器: 监听键盘事件,根据按键改变相机的位置和旋转。 我们使用Math.cos和Math.sin来计算相机在x和z轴上的移动量,以实现平滑的移动。
碰撞检测
虽然 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事件监听器: 当鼠标被锁定时,监听鼠标移动事件,根据鼠标移动的距离更新rotationX和rotationY。pointerlockchange事件监听器: 监听鼠标锁定状态的改变。
总结:用CSS构建3D世界
通过巧妙地运用CSS的transform属性,我们能够构建出简单的可交互3D场景,并模拟第一人称视角。虽然这种方法存在性能限制,但它在创建轻量级3D效果和增强UI交互方面具有潜力。深入理解3D变换的数学原理是关键,这将帮助我们更好地利用CSS的强大功能。
更多IT精英技术系列讲座,到智猿学院