用CSS构建生成式3D地形世界

本文深入探讨如何仅使用CSS创建3D地形生成器,通过堆叠网格和3D变换技术实现高度图可视化,涵盖形状语法扩展、纹理光照处理和性能优化等核心技术细节。

用CSS构建生成式3D地形世界

等轴投影总能唤起一种温馨怀旧的感觉,这很可能源于90年代像素艺术经典游戏(从《Populous》到《Transport Tycoon》)将这种美学深深烙印在我们集体记忆中的浪潮。

在本文中,我们将探索如何用现代CSS重现同样的魅力。具体来说,我们将深入了解新发布的Layoutit地形生成器的内部机制,学习如何结合堆叠网格和3D变换在浏览器中创建完全可寻址的3D空间。

搭建场景

.scene元素充当我们的摄像机支架:这是通过perspective属性开始定义深度的起点。通过设置较大的值(8000px),我们获得了近乎等轴测的视角,并带有轻微的自然失真。这个父容器的每个子元素都继承transform-style: preserve-3d,这基本上确保了3D变换按预期工作。

.floor元素定义了世界的倾斜度。通过应用transform: rotateX(65deg) rotate(45deg),我们将整个空间倾斜进入视图,建立摄像机的方向。在这个基础上,多个.z元素通过translateZ(25px * level)垂直堆叠。这样,每个层在特定高度(唯一的Z级别)充当网格切片,而行和列定义了X和Y坐标。

这些元素共同创建了我们将放置形状的3D网格。从这个基础开始,我们的地形可以开始上升了!

1
2
3
4
5
6
7
8
<div class="scene">
  <div class="floor">
    <div class="z" style="transform: translateZ(0px);"></div>
    <div class="z" style="transform: translateZ(25px);"></div>
    <div class="z" style="transform: translateZ(50px);"></div>
    <div class="z" style="transform: translateZ(75px);"></div>
  </div>
</div>
1
2
3
4
5
6
7
8
.scene { perspective: 8000px; }
.scene * { transform-style: preserve-3d; }
.floor { transform: rotateX(65deg) rotate(45deg); }
.z {
  display: grid;
  grid-template-columns: repeat(32, 50px);
  grid-template-rows: repeat(32, 50px);
}

扩展形状语法

除了简单的立方体,我们的世界需要新的图元:我们称它们为平面、斜坡、楔形和尖峰,它们是地形生成的最小单位。

每个形状倾斜一个或两个平面来定义其形式。它们遵循2:1的二测投影系统,其中每个高度单位等于两个深度单位。实际上,这产生了测量为50×50×25px的单元格。常见的面倾斜角度arctan(0.5) ≈ 26.565°保持了跨图块的几何一致性,确保了干净的阴影过渡和相邻单元格之间的无缝斜坡。

让我们仔细看看每个形状是如何组合的:

平面形状

平面保持水平;它只是一个在Z维度上平移25px并旋转以匹配其基本方向的平面。

1
2
3
.tile.flat {
  transform: translateZ(25px) rotate(0deg);
}

斜坡形状

斜坡重用相同的平面容器,但添加了一个矩形面伪元素,倾斜26.565°以创建斜坡。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.tile.ramp {
  transform: translateZ(25px) rotate(0deg);
}
.tile.ramp::before {
  content: "";
  position: absolute;
  inset: 0;
  transform-origin: top left;
  transform: rotateY(26.565deg);
}

楔形形状

楔形结合了斜坡的倾斜面和一个旋转90度的镜像面,在它们之间创建凹形连接。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
.tile.wedge {
  transform: translateZ(25px) rotate(0deg);
}
.tile.wedge::before,
.tile.wedge::after {
  content: "";
  position: absolute;
  inset: 0;
  transform-origin: top left;
}
.tile.wedge::before {
  transform: rotateY(26.565deg);
}
.tile.wedge::after {
  transform: rotate(-90deg) scaleX(-1) rotateY(26.565deg);
}

尖峰形状

尖峰镜像斜坡以形成峰顶。它结合了两个相对的斜坡:前斜坡向内倾斜,镜像的一个上升,直到它们在凸脊处相遇。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
.tile.spike {
  transform: translateZ(25px) rotate(0deg);
}
.tile.spike::before,
.tile.spike::after {
  content: "";
  position: absolute;
  inset: 0;
  transform-origin: top left;
}
.tile.spike::before {
  transform: rotateY(26.565deg);
  transform-origin: bottom left;
}
.tile.spike::after {
  transform: translateZ(-25px) rotateX(26.565deg);
}

纹理和光照

由于我们的形状只是普通的DOM元素,我们可以轻松地用CSS样式化它们。在这种情况下,使用background-imagebackground-color是最佳选择,因为它不会添加新节点(如<img><svg>会)。作为折衷方案,在需要动画或交互时,向选定形状添加内联SVG可能是有意义的。

此引擎中的光照是方向性的并烘焙到纹理中。我们将光源固定在西侧(180°),并根据每个可见面与该光源的角度将其分类为四个亮度带之一。每个形状根据其相对于光源的方向接收一个光照级别类(.l-1.l-4)。结果是可信的阴影,即使在场景旋转时也保持一致。

生成噪声

地形是一个高度图:一组由噪声构建并塑造成粗略陆地的二维高程值数组。初始原始场来自像simplex-noise这样的库,随后进行许多细化处理。这平滑了斑点,将陡峭区域台阶化,并限制了陡峭度的变化程度。这个世界的黄金法则之一是图块的高度差不能超过一个级别,这保持了斜坡的一致性并防止形成悬崖。

在用户侧,暴露了两个主要旋钮:陆地覆盖率,控制填充地图的水的百分比;和地形类型,设置场景中高程的上限。

一旦高度图构建完成,分类器决定哪个形状适合每个单元格。图块最多可以有八个可能的邻居,每个有四个旋转状态,这很快增加了数百种组合。为了处理所有这些复杂性,规则手册定义了形状应如何在每个基本点相遇。当这些规则仍然不足时(如在尖锐交叉点或极端斜坡上),一组手动策划的覆盖步骤介入清理并使地形保持稳定。

性能注意事项

堆叠网格的主要瓶颈之一是它们可以容纳的DOM元素数量。每个图块、面和层都会累加,当我们渲染大型地形时,浏览器已经在处理数千个节点。32×32×12网格大致是大多数现代系统的安全限制;超出此范围,渲染变得不可预测,帧速率下降,图块可能闪烁或完全消失。

使用clip-path绘制楔形和尖峰的三角形面带来的真正痛点。它看起来干净且纯CSS,但它迫使浏览器在每次场景旋转时重新绘制,拖累性能。修复方法是切换到具有透明背景的预切割PNG精灵。在浏览器适当优化3D上下文中的clip-path之前,精灵仍然是最可靠的选择。

下一步

除了作为一个伟大的技术挑战外,这个项目证明了堆叠网格技术可以远远超出立方体。添加斜坡和角度开启了一种新的深度:实际上由光和形状塑造的3D体积,即使一切仍然只是CSS。

从这里开始,有许多路径可以探索。等轴测网页游戏是一个明显的方向,还有生活在浏览器中的轻量级交互体验。目标不是取代WebGL,而是探索一种不同的构建3D项目的方式,保持简单、可读和可检查。

至于我的下一个3D网格项目,可能涉及将地形内外翻转:镜像两个垂直网格,使用重复的高度图形成单个连续体积。也许这就是我们实现真正的CSS球体的方式。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计